Skip to content

Expose JA4 through a standard request interface - #469

Open
dhruv8sh wants to merge 7 commits into
mainfrom
spec/283-ja4-request-interface
Open

dhruv8sh wants to merge 7 commits into
mainfrom
spec/283-ja4-request-interface

Conversation

@dhruv8sh

@dhruv8sh dhruv8sh commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a provider-neutral JA4 TLS fingerprint interface (Expose JA4 through a standard request interface #283): ctx.ja4() -> Option<&CapturedJa4>, backed by the adapter-owned RequestIngress metadata from Add pre-dispatch hooks and explicit request ingress fidelity #403. Today the only way to get JA4 is the Fastly-only raw-request hook, and apps can't tell "this platform never supplies JA4" from "this request has none".
  • Fastly (get_tls_ja4()) and Cloudflare (Bot Management ja4) populate it. Spin reports NotExposed because it never sees the TLS handshake. Axum reports NotExposed by default. It can instead read JA4 from a TLS-terminating proxy's header (FoxIO's nginx module, HAProxy, ATS, CloudFront), but only when the direct TCP peer is in an operator-configured CIDR allowlist. Client-supplied headers are never trusted, and app hooks can't overwrite the value.
  • Includes the design spec, the implementation plan, the implementation, tests on every runtime, and docs. Only JA4 (BSD-3-Clause) is in scope; the other JA4+ methods are under the FoxIO License 1.1.

Changes

Crate / File Change
crates/edgezero-core/src/ja4.rs (new) Ja4Fingerprint (strict 36-byte a_b_c validation, summary / cipher_hash / extension_hash, redacted Debug, no Display/Serialize), CapturedJa4 (Captured { fingerprint, source } or Unavailable(reason)), Ja4Source::RuntimeTls, Ja4Unavailable::{Malformed, Missing, NotExposed}, CapturedJa4::from_runtime_tls, Ja4Source::TrustedProxyHeader, CapturedJa4::from_trusted_proxy_header.
crates/edgezero-core/src/ingress.rs RequestIngress gains with_ja4 / ja4(). None means the adapter made no claim. Debug shows the state, never the value.
crates/edgezero-core/src/context.rs RequestContext::ja4().
edgezero-adapter-fastly capture_request_ingress records get_tls_ja4().
edgezero-adapter-cloudflare into_core_request records cf().bot_management().ja4().
edgezero-adapter-spin Records Unavailable(NotExposed).
edgezero-adapter-axum New ja4.rs: TrustedJa4Header (header name + CIDR allowlist via ipnet 2, already in the lockfile). Configured by EDGEZERO__ADAPTER__JA4_HEADER + EDGEZERO__ADAPTER__JA4_TRUSTED_PROXY_CIDRS, or AxumDevServer / EdgeZeroAxumService::with_trusted_ja4_header. Fails closed on partial config, invalid entries and /0. Without config it records NotExposed as before.
tests/fixtures/request-fidelity{,-runner} Hook reports a fixed ja4_state label. The spoof case sends ja4 and cloudfront-viewer-ja4-fingerprint headers. The runner asserts the per-platform state. Observations re-recorded; the only change is the new field.
docs/guide/adapters/*.md New "Client TLS fingerprint (JA4)" section in the overview (example, capability matrix, trust model, FoxIO attribution). The Fastly raw-request example now uses the H2 fingerprint. Cloudflare and Spin notes. New Axum section "JA4 from a trusted proxy" with an nginx example and a warning that the proxy must overwrite the header.
docs/superpowers/{specs,plans}/ Design spec and implementation plan with verification log.

Closes

Closes #283

Test plan

  • cargo test --workspace --all-targets: 1566 passed
  • cargo clippy --workspace --all-targets --all-features -- -D warnings on the pinned Rust 1.98.1, plus clippy on wasm32-unknown-unknown (Cloudflare), wasm32-wasip1 (Fastly) and wasm32-wasip2 (Spin)
  • cargo fmt --all -- --check, and for both fixture workspaces
  • cargo check --workspace --all-targets --features "fastly cloudflare spin"
  • WASM runtime tests: Fastly lib under Viceroy (98 passed); Cloudflare contract under wasm-bindgen-test-runner + Chromium (14 passed); Spin contract under wasmtime 48.0.1 (12 passed)
  • examples/app-demo workspace: 33 passed
  • Docs build: cd docs && npm run lint && npm run format && npm run build
  • Axum trusted proxy: unit tests for the peer matrix (IPv4, bare IP, IPv4-mapped IPv6, IPv6, untrusted, unknown), Missing / Malformed, and config rejection; a real-TCP dev-server test where a loopback client is captured under 127.0.0.0/8 and NotExposed under 192.0.2.0/24
  • Manual testing: edgezero demo (Axum) serves GET / → 200 with and without the JA4 config, and refuses to start with only one JA4 variable set or with 0.0.0.0/0
  • Other: ./scripts/test-request-fidelity.sh all against real Spin 4.1.0, Viceroy 0.21.0 and workerd. JA4 state is missing on Fastly and Cloudflare (local runtimes serve plain HTTP) and not_exposed on Spin, including the spoofed-header case. This covers Spin's into_core_request path, which the plain-wasmtime suite can't construct.
  • Live provider check (needs credentials and a deployment): Fastly over HTTPS → Captured, over HTTP → Missing; Cloudflare zone with Bot Management → Captured, without → Missing. Also confirms providers emit lowercase hex.

Checklist

  • Changes follow CLAUDE.md conventions
  • No Tokio deps added to core or adapter crates
  • Route params use {id} syntax (not :id)
  • Types imported from edgezero_core (not http crate)
  • Store wiring goes through KvRegistry / ConfigRegistry / SecretRegistry (not the legacy single-handle setters) — see spec §6.6
  • New code has tests
  • No secrets or credentials committed

Specify a provider-neutral JA4 accessor (ctx.ja4()) backed by adapter-owned
RequestIngress metadata: Fastly and Cloudflare populate it from their trusted
runtime APIs, Spin and Axum report NotExposed, and no adapter derives JA4 from
request headers.

Refs #283
@dhruv8sh dhruv8sh self-assigned this Nov 12, 2026
aram356 added a commit to IABTechLab/trusted-server that referenced this pull request Nov 12, 2026
Per the user, ID-7 relies on a trusted JA4 where the adapter supplies one
and otherwise does not block record creation (DEC-18), and JA4 is an
EdgeZero abstraction, ctx.ja4(), filled per adapter: Fastly from runtime
TLS, Cloudflare from Bot Management, Axum from a trusted proxy header such
as CloudFront's, Spin never. Trusted Server reads JA4 only through it, so
T12 deletes the direct Fastly read. IABTechLab/edgezero#469's draft leaves
Axum NotExposed and defers the trusted proxy header to its section 13.1;
the plan asks for that section in R2.
@aram356

aram356 commented Nov 13, 2026

Copy link
Copy Markdown
Collaborator

CREATOR: From the server-side identity plan (IABTechLab/trusted-server#1559, c7dc9de77): Bring section 13.1, JA4 from a trusted proxy header recorded as Ja4Source::TrustedProxyHeader, into the work that ships in R2b. Axum's ID-7 needs JA4 from a hop that #409 or #439 checks, such as CloudFront's CloudFront-Viewer-JA4-Fingerprint; an unchecked peer stays NotExposed (EZI-3, DEC-18).

Part of #283

@dhruv8sh
dhruv8sh marked this pull request as ready for review November 13, 2026 16:15
@dhruv8sh dhruv8sh linked an issue Nov 13, 2026 that may be closed by this pull request
4 tasks
Add edgezero_core::ja4 with a validated, Debug-redacted Ja4Fingerprint
and CapturedJa4 provenance/absence states. RequestIngress carries an
optional capture and RequestContext::ja4() reads it.

Fastly and Cloudflare populate it from their runtime TLS APIs; Spin and
Axum report NotExposed. No adapter derives JA4 from request headers.

Includes the implementation plan and adapter documentation.

Refs #283
Add a fixed ja4_state label to the request-fidelity fixture hook and send
spoofed ja4 / cloudfront-viewer-ja4-fingerprint headers in its spoof case.
The runner asserts Fastly and Cloudflare report missing and Spin reports
not_exposed on the real local runtimes, covering Spin's into_core_request
path that the plain-wasmtime contract suite cannot construct.

Replace magic section offsets in ja4.rs with named constants shared by
parse and the accessors, use matches! byte ranges, share one FoxIO sample
constant across core tests, and move test imports to module level.

Refs #283
@dhruv8sh dhruv8sh changed the title Add JA4 request interface design spec Expose JA4 through a standard request interface Nov 13, 2026
Axum never sees the TLS handshake, but a TLS-terminating proxy in front of
it (FoxIO's nginx module, HAProxy, ATS, CloudFront) can compute JA4. Add
Ja4Source::TrustedProxyHeader and CapturedJa4::from_trusted_proxy_header,
and an Axum TrustedJa4Header that records the configured header only when
the direct TCP peer is inside an operator CIDR allowlist. Any other peer
stays NotExposed, so clients cannot forge it.

Configured with EDGEZERO__ADAPTER__JA4_HEADER and
EDGEZERO__ADAPTER__JA4_TRUSTED_PROXY_CIDRS, or programmatically via
AxumDevServer / EdgeZeroAxumService::with_trusted_ja4_header. Config fails
closed on partial settings, invalid entries and /0 networks. Default
behaviour is unchanged.

Spec §5.8, plan task 6 and the Axum guide document the design, the
operator requirement to overwrite the header, and an nginx example.

Refs #283

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary

A well-structured design: one adapter-owned ctx.ja4() with explicit provenance and explicit "no fingerprint" states, and request headers are never trusted by default. The new Axum trusted-proxy path is careful and fails closed.

I'm requesting changes for three reasons:

  • The docs present CloudFront as a source for a plain IP allowlist, and any CloudFront distribution can pass that check.
  • The Fastly/Cloudflare capture path still has no test that could fail if it broke.
  • The separate JA4 proxy allowlist departs from the requested #409/#439 trust check, and that needs a maintainer decision.

😃 Praise

  • The Axum trust check is well protected. I applied five deliberate breaks, and each was caught:
    • Trust any peer: caught by the peer-matrix test and the real-TCP dev-server test.
    • Trust an unknown peer: caught.
    • Skip IPv4-mapped canonicalization: caught.
    • Allow /0 networks: caught.
    • Ignore the config in into_core_request: caught by the real-TCP test.
  • Config fails closed on partial settings, invalid entries and /0. A repeated header field is treated as Malformed rather than picking one.
  • Ja4Fingerprint::parse is tight. It uses named offsets, never echoes its input, and has good rejection cases.
  • The request-fidelity spoof case is strong real-runtime evidence. At a3afa231, the CI logs show ja4_state asserted in 16 Fastly, 12 Cloudflare and 13 Spin results, checked against the committed observations.json.
  • On every production Fastly path, the ingress snapshot is inserted after hook values are merged (request.rs:330, :453).

Findings

Blocking

  • 🔧 CloudFront is listed as a source for the IP allowlist, which any CloudFront distribution can pass. See inline on docs/guide/adapters/axum.md.
  • 🔧 The tests can't tell real Fastly/Cloudflare JA4 wiring from a constant Missing. See inline on crates/edgezero-adapter-cloudflare/tests/contract.rs.
  • ❓ A separate JA4-only proxy allowlist. aram356 asked for JA4 "from a hop that #409 or #439 checks". #409 adds EDGEZERO__ADAPTER__TRUSTED_PROXY_CIDRS, and this PR deliberately adds a second EDGEZERO__ADAPTER__JA4_TRUSTED_PROXY_CIDRS. Spec §5.8 explains why: it's a narrower claim, and it avoids depending on when #409 merges. That's reasonable, but operators would maintain two IP lists for the same proxies, and the lists can drift. It also departs from the stated requirement. @aram356, is the separate setting acceptable, or should this path wait for #409's CheckedProxyMetadata?

Non-blocking

  • 🤔 Every Fastly request now loads the full client TLS metadata bundle. See inline.
  • 🤔 An empty provider value is classified as Malformed. See inline.
  • ⛏ Some parser rules and the redaction of section a are not protected by tests. See inline.
  • 🤔 Spec §10 lists Fastly and Spin tests that weren't added. See inline.
  • ⛏ Nothing tests that run_app reads the JA4 environment variables. See inline.
  • 📝 The nginx warning misses the proxy_set_header inheritance trap. See inline.
  • ⛏ The overview matrix is slightly wrong for configured Axum. See inline.
  • 📝 The PR description says "app hooks can't overwrite the value". That holds for the Fastly raw-request hook only. Pre-dispatch hooks receive &mut Request and can replace extensions. The overview doc and spec §5.7 already state this correctly.

CI Status

  • Hosted checks for 304f5a42 were still queued when this review was written.
  • I ran these locally on 304f5a42:
    • cargo test -p edgezero-core --lib: 511 passed.
    • cargo test -p edgezero-adapter-axum --all-targets --features axum: 131 passed.
    • My local Rust is 1.99.1; the repo pins 1.98.1.
  • I did not re-run the wasm suites locally. Their code is unchanged since a3afa231, where they passed in CI.
  • No live Fastly or Cloudflare Captured result has been verified yet.


When a TLS-terminating proxy in front of Axum computes JA4 (FoxIO's
[ja4-nginx-module](https://github.com/FoxIO-LLC/ja4-nginx-module), HAProxy's
ja4 module, the Apache Traffic Server JA4 plugin, CloudFront, …), Axum can read

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 CloudFront can't be safely identified by an IP allowlist.

CloudFront's IP ranges are shared by every AWS customer. Anyone can create their own distribution that points at this origin. Its requests arrive from an allowlisted CloudFront IP, so they pass the peer check. That distribution can also add arbitrary headers through its "origin custom headers" setting.

With CLOUDFRONT ranges in JA4_TRUSTED_PROXY_CIDRS, an attacker could therefore get any value they like recorded as Captured { source: TrustedProxyHeader }. This is why #439 has a separate protected-CloudFront check. I haven't checked whether CloudFront rejects a custom header named CloudFront-Viewer-*, but the operator can choose any header name.

Suggested fix: remove CloudFront from the allowlist-path examples here, in the ja4.rs module doc, in spec §5.8 and in the PR description. Alternatively, state that CloudFront needs #439's protected-CloudFront check, which this path doesn't provide. nginx, HAProxy and ATS are fine examples because the operator owns them.

}

#[wasm_bindgen_test]
async fn ingress_ja4_is_missing_without_cf_despite_header() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 These tests can't distinguish real JA4 wiring from a constant Missing.

This test (and the Fastly one, ingress_ja4_is_missing_for_non_client_request_despite_header) uses input where the provider can't supply a fingerprint:

  • Cloudflare: there is no cf object.
  • Fastly: FastlyRequest::get(...) is not a client request, and fastly 0.13.1's get_client_tls_metadata() returns None immediately (if !self.is_from_client()).
  • Real-runtime fixture: it serves plain HTTP, so both providers report missing there as well.

So replacing the adapter wiring with CapturedJa4::from_runtime_tls(None) still passes the whole suite. The cf → bot_management → ja4 chain and the adapter-level Captured/Malformed outcomes are never executed.

Suggested fix (Cloudflare): worker::Request::from(web_sys::Request) reads cf through a JS getter (worker-sys 0.8.5, ext/request.rs). That means a test can inject one before converting the request:

let bot = js_sys::Object::new();
js_sys::Reflect::set(&bot, &"ja4".into(), &"t13d1516h2_8daaf6152771_e5627efa2ab1".into()).unwrap();
let cf = js_sys::Object::new();
js_sys::Reflect::set(&cf, &"botManagement".into(), &bot).unwrap();
js_sys::Reflect::set(&raw, &"cf".into(), &cf).unwrap();
// expect Some(Captured { source: RuntimeTls, .. }), then repeat with an invalid value → Malformed

I traced this from the source but haven't run it, so it's worth confirming in the browser runner.

Fastly: this path can't be exercised without a TLS client. Please either note that limit explicitly or complete the live-provider check before merge.

None
};
RequestIngress::new(target, origin, HeaderFidelity::default())
.with_ja4(CapturedJa4::from_runtime_tls(req.get_tls_ja4()))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 This now loads the whole client TLS metadata bundle on every Fastly request.

In fastly 0.13.1, get_tls_ja4() goes through get_client_tls_metadata(), and its lazy load fetches all of these together:

  • the raw ClientHello
  • SNI, JA3 and JA4
  • the raw client certificate and its verification result
  • the cipher name and protocol

On main, the adapter never touched TLS metadata. Because capture_request_ingress runs on every dispatch path, every app now pays for those host calls and buffer copies, even if it never calls ctx.ja4(). Every app also now runs the SDK's panic!s in that bundle (for example, "must be valid UTF-8" on SNI and JA4).

The cost is probably small, but the spec doesn't mention it. Could you note this trade-off in the spec, or measure it, so it's a deliberate decision?

/// Map a runtime TLS accessor's result. Invalid values are discarded.
#[inline]
#[must_use]
pub fn from_runtime_tls(value: Option<&str>) -> Self {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 An empty provider value becomes Malformed, not Missing.

Cloudflare's JA3/JA4 docs say the value "can be null or empty" in some cases, for example when Bot Management is skipped or a TLS session is resumed. That statement is about logs; I haven't confirmed what Workers expose. worker-sys maps a JS "" to Some(""), so validated("") fails and the result is Unavailable(Malformed). An empty trusted-proxy header field behaves the same way.

The spec presents Malformed as a signal that the provider changed its format. Apps that treat it as suspicious would then flag ordinary requests.

Suggestion: treat an empty value as Missing, for example by filtering value.filter(|v| !v.is_empty()) before validating. Please also add a test for from_runtime_tls(Some("")).

// Cipher and extension counts.
4..=7 => byte.is_ascii_digit(),
// First and last ALPN characters.
8 | 9 => byte.is_ascii_alphanumeric(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ Some parser rules and the redaction of section a aren't protected by tests.

I applied each of these one at a time and ran cargo test -p edgezero-core --lib. All passed:

  • ALPN positions 8 | 9 => true
  • version positions 1 | 2 => true
  • a Debug that prints Ja4Fingerprint([redacted] t13d1516h2)

The protocol, SNI, count and uppercase-hex mutations were caught. The redaction tests only check that the full string and 8daaf6152771 are absent, but the spec says Debug never contains the fingerprint.

Suggestion:

  • Add rejection cases such as t13d1516h_… (ALPN) and t_3d… / tX3d… (version).
  • Assert that !output.contains("t13d1516h2") in ja4_never_prints_fingerprint_or_input and in the RequestIngress debug test.

| ----------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Core `ja4.rs` | native `cargo test -p edgezero-core` | • Accepts FoxIO's example `t13d1516h2_8daaf6152771_e5627efa2ab1`, a `q` value, a `d` value, `i` SNI, `000000000000` sections and uppercase ALPN characters.<br>• Section accessors return `t13d1516h2`, `8daaf6152771` and `e5627efa2ab1`.<br>• Rejects: empty, 35 and 37 bytes, leading or trailing whitespace, uppercase hex in `b` or `c`, wrong or missing separators, an unknown protocol or SNI letter, non-digit counts, a non-ASCII value of the right byte length, a 32-character JA3 hash and a `ja4_r` string.<br>• `from_runtime_tls` for `None`, valid and invalid input.<br>• `Debug` output of `Ja4Fingerprint`, `CapturedJa4` and `RequestIngress` never contains the fingerprint, and `parse` errors never contain the input. |
| Core `ingress.rs`, `context.rs` | native | • `RequestIngress::new(…).ja4()` is `None`; `with_ja4` round-trips.<br>• `RequestContext::ja4()` is `None` without `RequestIngress` and returns the stored value when present. |
| Fastly | Viceroy (`wasm32-wasip1`) | • `capture_request_ingress(&FastlyRequest::new(…))` gives `Some(Unavailable(Missing))`.<br>• Through `dispatch_with_registries`, a raw-request hook that inserts a `RequestIngress` with a fake captured JA4 does not change what the handler sees (modelled on the existing hook-override test at `crates/edgezero-adapter-fastly/src/request.rs:1153`).<br>• A client `ja4` header does not produce `Captured`. |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 This row lists tests that aren't in the PR.

The Fastly row describes a raw-request hook that inserts a RequestIngress with a fake captured JA4 through dispatch_with_registries. The Spin row describes a wasmtime contract test with a ja4 header. Neither was added.

The behaviour is covered elsewhere:

  • Fastly: the generic ingress_registry_dispatch_wins_scratch_collision test protects the replacement order.
  • Spin: the real-runtime fixture covers into_core_request.

R5's acceptance points at this table, so please either add the Fastly test or update these rows to name the coverage that exists.

log::warn!("{warning}");
}
let addr = resolution.addr;
let trusted_ja4_header = TrustedJa4Header::from_env(&env)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ Nothing tests that run_app reads the environment variables.

I replaced this line with let trusted_ja4_header: Option<TrustedJa4Header> = None;, and the Axum suite (131 tests) and the CLI suite still passed. from_env itself is unit-tested, and the PR description mentions a manual edgezero demo check.

What isn't tested automatically is the documented operator path: set both variables and capture works, or set only one and startup fails. A small test through run_app or serve_with_stores with an EnvConfig would cover it.

omits it when the value is empty:

```nginx
proxy_set_header X-JA4 $http_ssl_ja4;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Worth adding the nginx inheritance trap to this warning.

proxy_set_header directives are inherited from an enclosing block only when the current block defines none. So a location that sets its own proxy_set_header Host … silently drops an X-JA4 defined at the server or http level. The client's own X-JA4 then reaches Axum from a trusted peer.

I'm going by nginx's documented inheritance rule here; I haven't run it. Suggested addition: "Set it in every block that has its own proxy_set_header, and in every server block that proxies to Axum, including plain-HTTP ones."

| Fastly | `Captured { RuntimeTls }` | `Unavailable(Missing)` | `Unavailable(Malformed)` |
| Cloudflare | `Captured { RuntimeTls }` with Enterprise Bot Management; otherwise `Unavailable(Missing)` | `Unavailable(Missing)` | `Unavailable(Malformed)` |
| Spin | `Unavailable(NotExposed)` | `Unavailable(NotExposed)` | n/a |
| Axum | `Unavailable(NotExposed)`; with a [trusted proxy](./axum#ja4-from-a-trusted-proxy), `Captured { TrustedProxyHeader }` | `Unavailable(NotExposed)` | `Unavailable(Malformed)` |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ The Axum row is slightly wrong when a proxy is configured.

With a trusted proxy configured, a request from that proxy without the header gets Unavailable(Missing), not NotExposed. The "Plain HTTP" column (Axum is always plain HTTP) only shows NotExposed. Spec §7 states this correctly, as "From a trusted peer: … or Missing without the header".

Both workspaces depend on edgezero-adapter-axum by path, so adding ipnet
to it requires their lockfiles to record the new edge. CI runs them with
--locked. No new packages are resolved.

Refs #283

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

One item, found while bringing this PR into rc/identity (#495). It is the only reason #495's CI is red.

The inline comment carries a one-click GitHub suggestion. Use Commit suggestion to apply it.

Blocking

  • 🔧 The Axum guide names EdgeZeroAxumService, which #275 removes (docs/guide/adapters/axum.md:350-351)

Comment thread docs/guide/adapters/axum.md Outdated
Comment on lines +350 to +351
pass it to `AxumDevServer::with_trusted_ja4_header` or
`EdgeZeroAxumService::with_trusted_ja4_header`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrench: EdgeZeroAxumService does not exist once this meets #275. #275 removes the type (downstream response delivery is adapter-owned) and adds scripts/check_outbound_legacy_api.sh, which rejects the name anywhere in docs/guide. This PR targets main, where the type still exists, so its own CI has no such check and stays green.

Merging this PR into rc/identity (0ea3e7bc) turned #495's workspace tests job red at the "No legacy outbound API" step, on exactly this line (job log). The previous rc/identity head, 3964365e, was green. In that merge the builder moved to the crate-private AxumServiceState (service.rs:268 on rc/identity), so AxumDevServer::with_trusted_ja4_header is the only public way to set it there.

Dropping the second name is safe on main too: AxumDevServer::with_trusted_ja4_header is public on both bases (dev_server.rs, through pub mod dev_server). When this rebases onto #275, move the service.rs builder off EdgeZeroAxumService the same way the rc/identity merge did.

Suggested change
pass it to `AxumDevServer::with_trusted_ja4_header` or
`EdgeZeroAxumService::with_trusted_ja4_header`.
pass it to `AxumDevServer::with_trusted_ja4_header`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied the suggestion in 52f4a09f.

aram356 added a commit that referenced this pull request Nov 14, 2026
Brings #275 at e9ffaa8 and the refreshed heads of #499, #460, #406,
#407, #409, #414, #422 and #439. Each PR merged the one below it, so
this merge has no conflicts.

Two edits fit #469 to #275, as #469's open review asks:

- #469's ingress JA4 test calls into_core_request with one argument.
  #275 now passes the Axum outbound executor through that function, so
  the test gets an executor handle the way the other ingress tests do.
- The Axum guide names EdgeZeroAxumService, which #275 removes and
  check_outbound_legacy_api.sh rejects. The guide now names only
  AxumDevServer::with_trusted_ja4_header.
EdgeZeroAxumService does not exist once this meets #275, and #275's check_outbound_legacy_api.sh rejects the name in docs/guide. AxumDevServer::with_trusted_ja4_header is public on both main and #275.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose JA4 through a standard request interface

3 participants