Skip to content

feat: optional CSRF layer + embedded-asset (single-binary) helper#1

Merged
adiologydev merged 18 commits into
mainfrom
feat/csrf-and-embedded-assets
May 26, 2026
Merged

feat: optional CSRF layer + embedded-asset (single-binary) helper#1
adiologydev merged 18 commits into
mainfrom
feat/csrf-and-embedded-assets

Conversation

@adiologydev

@adiologydev adiologydev commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Two opt-in, feature-gated additions surfaced as feedback from a downstream app. Both are off by default and change no existing behavior.

1. csrf — Inertia/axios-compatible CSRF protection

The Inertia client (axios) reads an XSRF-TOKEN cookie and echoes it back as the X-XSRF-TOKEN header on every mutating request — no frontend code needed. This adds the server side of that convention:

  • CsrfTokens (src/csrf/mod.rs) — framework-agnostic, stateless signed double-submit token minter/verifier. HMAC-SHA256 (reusing the cookie-session crypto stack), getrandom entropy, subtle constant-time comparison. No server-side session required. The HMAC signature makes it stronger than plain double-submit: an attacker able to write cookies (sibling subdomain, MITM over HTTP) still can't forge a server-issued token.
  • CsrfLayer (src/adapters/axum/csrf.rs) — a standalone tower layer composed next to InertiaLayer. Issues a JS-readable XSRF-TOKEN cookie (Path=/, SameSite=Lax, Secure-by-default, not HttpOnly), verifies the header on POST/PUT/PATCH/DELETE, and short-circuits 419 before the handler runs on mismatch. Builder: .secure(), .same_site(), .cookie_name(), .header_name(), .exclude() (for webhooks).
let app = router()
    .with_state(state)
    .layer(InertiaLayer::new(cfg))
    .layer(CsrfLayer::new(secret));   // outermost: verifies first, sets cookie last

2. embed — single-binary embedded assets

ViteRootView::production emits asset tags but the bytes had to come from ServeDir (disk). For single-binary deploys (rust-embed / include_dir), the manifest already embeds via include_str!(...).parse(); this adds the byte-serving half:

  • EmbeddedAssets (src/adapters/axum/embed.rs) — an infallible axum Service for nest_service, built from a resolver closure Fn(&str) -> Option<Cow<'static, [u8]>>. Works with rust-embed, include_dir, or a plain map — Veer depends on none of them. Content-type from a built-in extension map (.mime() to extend), immutable Cache-Control for content-hashed files, zero-copy for 'static borrowed bytes, Allow header on 405.
.nest_service("/build", EmbeddedAssets::new(|p| Assets::get(p).map(|f| f.data)))

Also included

  • Cargo.toml feature flags (csrf, embed) + new optional deps getrandom, subtle (the rest shared with cookie-session).
  • Crate-root + adapter re-exports; lib.rs + README feature-table rows; two README cookbook subsections.
  • CsrfLayer wired into the axum-react-todo example.
  • New CHANGELOG.md (Keep a Changelog format).

Testing

  • cargo test --all-features: all green — 60 lib unit tests, 9 csrf + 5 embed integration tests, plus the existing 6 axum / 1 conformance / 26 spec-coverage suites and 3 doctests.
  • cargo clippy --all-features --all-targets -- -D warnings: clean.
  • Default (no-features) build + test unaffected; example builds with csrf.
  • Verified each feature compiles in isolation (csrf alone, embed alone, both, all).

Review notes

Built via TDD, task-by-task, with per-task spec + code-quality review and a final holistic security review. CSRF-specific scrutiny: no path lets a mutating request reach the handler without a valid token; the 419 path reissues a cookie only when the client lacks a valid one (no needless rotation); constant-time verification throughout.

Note: the design/plan docs live under docs/superpowers/ which is gitignored, so they're not in this PR.

Summary by CodeRabbit

  • Release

    • v0.1.2 published.
  • New Features

    • CSRF protection: stateless double-submit tokens with JS-readable cookie, selectable exclusions, and 419 on mismatch.
    • Embedded assets: serve frontend build artifacts from the binary with content-type mapping and long-lived caching.
  • Documentation

    • README and changelog updated with feature flags, usage, and security/behavior notes.
  • Tests / Examples

    • Added end-to-end tests and example updates demonstrating CSRF and embedded-asset behavior.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 68e53487-72be-475c-81a9-dc2b0495f99c

📥 Commits

Reviewing files that changed from the base of the PR and between 0c005af and 9f4c555.

📒 Files selected for processing (7)
  • examples/axum-react-todo/Cargo.toml
  • examples/axum-react-todo/justfile
  • examples/axum-react-todo/src/lib.rs
  • src/adapters/axum/embed.rs
  • src/bindings/routes.rs
  • src/lib.rs
  • tests/embed.rs

📝 Walkthrough

Walkthrough

Adds feature-gated CSRF primitives and an Axum CsrfLayer, an EmbeddedAssets Tower service, tests for both features, README/CHANGELOG/Cargo updates, Axum adapter re-exports, and example wiring changes (routes, defaults, dev wait).

Changes

CSRF Protection and Embedded Assets

Layer / File(s) Summary
Docs, changelog, and Cargo updates
CHANGELOG.md, Cargo.toml, README.md, src/lib.rs
Bumps version to 0.1.2, adds csrf feature optional deps (getrandom, subtle), documents csrf and embed in README and changelog, and adds feature-gated re-exports.
CsrfTokens core
src/csrf/mod.rs
Implements signed double-submit tokens ({rand_b64}.{hmac}), constructor, generate/verify, ct-equality helper, and unit tests.
CsrfLayer Axum middleware
src/adapters/axum/csrf.rs, src/adapters/axum/mod.rs
Tower Layer/Service enforcing double-submit on mutating verbs, path exclusions, JS-readable cookie issuance (no HttpOnly), returns 419 on mismatch, and wires adapter export.
CSRF tests and example wiring
tests/csrf.rs, examples/axum-react-todo/src/main.rs, examples/axum-react-todo/Cargo.toml
Feature-gated integration tests covering issuance, verification, exclusions, rotation semantics; example app demonstrates CsrfLayer and adds csrf feature to example dependency.
EmbeddedAssets service implementation
src/adapters/axum/embed.rs, src/adapters/axum/mod.rs
Resolver-backed service supporting GET/HEAD, method allowlist, content-type resolution with overrides and safe fallback, immutable cache headers, and Tower integration; re-export behind embed.
EmbeddedAssets tests
tests/embed.rs
Feature-gated tests validating serving known asset, HEAD semantics, 404, 405, MIME override behavior, and fallback on malformed overrides.
Route param syntax & examples
src/bindings/routes.rs, examples/axum-react-todo/src/lib.rs, examples/axum-react-todo/justfile
Switches codegen to Axum 0.8 {...} capture syntax, updates example route to /todos/{id}, sets example default-run, and adds SSR sidecar startup wait in dev recipe.

Sequence Diagram(s):

sequenceDiagram
  participant Client
  participant CsrfLayer
  participant CsrfTokens
  participant InnerService
  Client->>CsrfLayer: HTTP Request (method, path, headers)
  CsrfLayer->>CsrfLayer: parse Cookie, check exclusion, is_mutating?
  alt Mutating & not excluded
    CsrfLayer->>CsrfLayer: read x-xsrf-token header
    CsrfLayer->>CsrfTokens: verify(cookie, header)
    alt verify == false
      CsrfLayer->>Client: 419 CSRF token mismatch (+ maybe Set-Cookie)
    else
      CsrfLayer->>InnerService: forward request
    end
  else
    CsrfLayer->>InnerService: forward request
  end
  InnerService->>CsrfLayer: Response
  CsrfLayer->>Client: Response (maybe Set-Cookie)
Loading

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~45 minutes

"I nibble bytes and sign each seed,
A token baked for every need.
I stash the assets in my lair,
Serve them swift with headers fair.
Hop, patch, and ship — the rabbit cheers!"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly summarizes the main changes: adding optional CSRF protection and embedded-asset serving capabilities as feature-gated functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/csrf-and-embedded-assets

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/adapters/axum/csrf.rs`:
- Around line 118-121: The call method is creating a fresh clone of self.inner
instead of using the instance that was poll_ready'd, breaking Tower
readiness/backpressure; change call to consume or take the already-ready inner
service rather than cloning (e.g., use std::mem::take or std::mem::replace on
self.inner inside call so you run the poll_ready'd instance), keeping the
existing config clone logic and returning the Box::pin future that uses that
taken inner service; ensure you reference the call method and self.inner so no
fresh clone is used.

In `@src/adapters/axum/embed.rs`:
- Around line 116-117: The function status (fn status(code: StatusCode) ->
Response<Body>) is not formatted to match rustfmt; run rustfmt (or cargo fmt)
and adjust the function formatting accordingly so the builder call and body are
split/indented per rustfmt rules (e.g., ensure
Response::builder().status(code).body(Body::empty()).unwrap() is formatted as
rustfmt expects). After formatting, re-run cargo fmt -- --check to confirm the
mismatch is resolved.
- Line 93: The response header currently always sets header::CACHE_CONTROL to
"public, max-age=31536000, immutable", which applies immutable caching to
non-fingerprinted assets; add a helper like is_content_hashed(path: &str) (use
the suggested heuristic) and change the .header(...) call in the embed response
generation so it sets "public, max-age=31536000, immutable" only when
is_content_hashed(path) is true and otherwise sets "public, max-age=31536000"
(reference the .header(header::CACHE_CONTROL, ...) invocation in
src/adapters/axum/embed.rs to locate where to branch).

In `@src/lib.rs`:
- Around line 104-106: Run rustfmt (cargo fmt) to fix the formatting/order
mismatch in the export block in src/lib.rs; ensure the pub use items (CsrfTokens
with #[cfg(feature = "csrf")] and SsrClient, SsrPayload) are formatted/ordered
per rustfmt rules so that cargo fmt --check passes.

In `@tests/embed.rs`:
- Around line 33-37: The test assertions in tests/embed.rs (the assert_eq!
checks that compare resp.headers().get("content-type") and
resp.headers().get("cache-control")) are not formatted to rustfmt style; run
rustfmt (or cargo fmt) to reformat the file (or run cargo fmt --tests to target
tests) so the assertion blocks at the ranges around the two assert_eq!s are
formatted, then re-run cargo fmt -- --check to verify; commit the reformatted
tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ff80151-b36d-41cc-80ad-c2820c50d991

📥 Commits

Reviewing files that changed from the base of the PR and between feffcfc and 03ebd1f.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • Cargo.toml
  • README.md
  • examples/axum-react-todo/Cargo.toml
  • examples/axum-react-todo/src/main.rs
  • src/adapters/axum/csrf.rs
  • src/adapters/axum/embed.rs
  • src/adapters/axum/mod.rs
  • src/csrf/mod.rs
  • src/lib.rs
  • tests/csrf.rs
  • tests/embed.rs

Comment thread src/adapters/axum/csrf.rs
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, ct)
.header(header::CONTENT_LENGTH, len)
.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate immutable caching to hashed assets only.

Line 93 applies immutable to every asset, which can lock browsers to stale content for non-fingerprinted filenames. This also diverges from the stated behavior (“immutable for content-hashed files”).

Suggested fix
-                    .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
+                    .header(
+                        header::CACHE_CONTROL,
+                        if is_content_hashed(path) {
+                            "public, max-age=31536000, immutable"
+                        } else {
+                            "public, max-age=0, must-revalidate"
+                        },
+                    )
fn is_content_hashed(path: &str) -> bool {
    // simple heuristic, can be replaced with a stricter matcher
    let name = path.rsplit('/').next().unwrap_or(path);
    name.split('.')
        .next()
        .is_some_and(|stem| stem.rsplit('-').next().is_some_and(|s| s.len() >= 8 && s.chars().all(|c| c.is_ascii_hexdigit())))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/axum/embed.rs` at line 93, The response header currently always
sets header::CACHE_CONTROL to "public, max-age=31536000, immutable", which
applies immutable caching to non-fingerprinted assets; add a helper like
is_content_hashed(path: &str) (use the suggested heuristic) and change the
.header(...) call in the embed response generation so it sets "public,
max-age=31536000, immutable" only when is_content_hashed(path) is true and
otherwise sets "public, max-age=31536000" (reference the
.header(header::CACHE_CONTROL, ...) invocation in src/adapters/axum/embed.rs to
locate where to branch).

Comment thread src/adapters/axum/embed.rs Outdated
Comment thread src/lib.rs Outdated
Comment thread tests/embed.rs Outdated
Comment thread src/adapters/axum/csrf.rs

fn path_excluded(path: &str, excludes: &[String]) -> bool {
excludes.iter().any(|p| {
let p = p.trim_end_matches('/');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Review · Medium] exclude() prefix matching has two silent footguns.

  1. Fail-open: .exclude("/") (or .exclude("")) → after trim_end_matches('/') the prefix is "". path.starts_with("") is always true and path.as_bytes().get(0) == Some(&b'/') holds for every request path, so every mutating request skips CSRF verification — a well-meaning .exclude("/") disables protection site-wide with no error.
  2. Fail-closed: .exclude("webhooks") (no leading slash) never matches /webhooks/... (paths always begin with /), so the endpoint keeps returning 419.

Consider rejecting/normalizing an empty-or-/ prefix and documenting that the argument must be a leading-slash path.

Comment thread src/csrf/mod.rs Outdated
/// Checks only the signature, not double-submit equality — prefer
/// [`Self::verify`] for CSRF validation. Exposed for callers that need a
/// standalone signature check (e.g. deciding whether to re-issue a cookie).
pub fn is_valid(&self, token: &str) -> bool {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Review · Medium] is_valid() is pub but only checks the HMAC signature — not the cookie↔header binding.

A caller who validates a request with is_valid(header_value) instead of verify(cookie, header) accepts any server-issued token regardless of the XSRF-TOKEN cookie, defeating the double-submit guarantee (an attacker can replay a token minted in their own session). The doc steers to verify, but the only in-crate consumer is the middleware. Consider pub(crate) fn is_valid to remove the public misuse vector entirely while keeping verify as the public validation entry point.

Comment thread src/adapters/axum/csrf.rs Outdated

fn call(&mut self, req: Request<Body>) -> Self::Future {
let cfg = self.config.clone();
let mut inner = self.inner.clone();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Review · Low] Tower readiness: call() drives a freshly-cloned inner that was never poll_ready'd.

poll_ready readies self.inner, but call clones self.inner and invokes the clone. With a readiness-stateful inner service (Buffer, ConcurrencyLimit, rate limiters) this bypasses backpressure accounting. Harmless with axum's always-ready Router (the intended use), and it mirrors the existing InertiaMiddleware in layer.rs, so it's consistent with the crate — flagging for completeness. Canonical fix: let mut inner = std::mem::replace(&mut self.inner, self.inner.clone()); so the polled instance is the one called.

Comment thread src/adapters/axum/csrf.rs
_ => false,
};
if !ok {
let mut resp = Response::new(Body::from("CSRF token mismatch"));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Review · Low] 419 short-circuit response sets no Content-Type.

Response::new(Body::from("CSRF token mismatch")) emits a text body with no Content-Type. The Inertia client ignores the 419 body (it reloads), so this is invisible there, but outer middleware (compression, body-decoding loggers) sees an untyped body. Cheap to set text/plain; charset=utf-8.

.header(header::CONTENT_TYPE, ct)
.header(header::CONTENT_LENGTH, len)
.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
.body(body)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Review · Low] A .mime() override with invalid header bytes panics on every matching request.

If content_type() returns a developer-supplied override containing CR/LF/NUL or non-ASCII, Response::builder().header(CONTENT_TYPE, …).body(body).unwrap() panics — the builder error surfaces at .body(). It's config-time input (not attacker-controlled), but a typo'd MIME string becomes a per-request panic for that extension. Consider validating in .mime() or handling the builder Result gracefully.

.status(StatusCode::OK)
.header(header::CONTENT_TYPE, ct)
.header(header::CONTENT_LENGTH, len)
.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Review · Low] immutable Cache-Control is applied unconditionally.

public, max-age=31536000, immutable is correct for Vite content-hashed filenames, but anyone serving a non-fingerprinted file (e.g. manifest.webmanifest, robots.txt) through EmbeddedAssets gets a year-long immutable cache with no revalidation path (no ETag/Last-Modified). This matches the documented "hashed assets only" intent — worth either stating that more loudly in the doc or offering an opt-out for non-hashed paths.

@adiologydev

Copy link
Copy Markdown
Member Author

Review summary

Reviewed at high effort (3 finder angles → verify). Overall: ready to merge — the core is sound and well-tested. No Critical/blocking issues. The CSRF design is a signed double-submit (HMAC + subtle constant-time compare + getrandom), which is stronger than plain double-submit, and there is no path where a mutating request reaches the handler without a valid token.

6 inline findings, all Medium or below — none block merge:

  • Mediumexclude() prefix footguns: .exclude("/")/"" fails open (disables CSRF site-wide); a missing leading slash fails closed.
  • MediumCsrfTokens::is_valid() is pub but checks only the signature; misuse (validating the header alone) bypasses the cookie↔header binding. Suggest pub(crate).
  • Low — Tower poll_ready/clone deviation (mirrors the existing InertiaLayer; harmless with axum's Router).
  • Low — 419 response has no Content-Type.
  • Low — a .mime() override with invalid header bytes panics per request.
  • Low — unconditional immutable Cache-Control (correct for hashed assets; risky for non-hashed files).

Validation that already passed on the branch: cargo test --all-features (all suites green, incl. 9 csrf + 5 embed integration tests), cargo clippy --all-features --all-targets -- -D warnings clean, default no-features build/test unaffected, example builds.

My recommendation: the two Medium items are worth a quick follow-up (guard the empty exclude prefix; make is_valid pub(crate)); the rest are optional polish.

- exclude(): normalize missing leading slash; ignore empty/"/" prefix
  instead of disabling CSRF site-wide (fail closed) + tests
- CsrfTokens::is_valid is now pub(crate) so it can't be misused as a
  validation entry point that skips the cookie<->header binding
- 419 response sets Content-Type: text/plain; charset=utf-8
- EmbeddedAssets: invalid .mime() override falls back to octet-stream
  instead of panicking the response builder + test
- CsrfMiddleware: drive the poll_ready'd inner via mem::replace (Tower
  contract) rather than an unpolled clone
- doc: note EmbeddedAssets assumes content-hashed filenames
@adiologydev

Copy link
Copy Markdown
Member Author

Review fixes applied — 67ce8f8

All 6 findings addressed:

# Finding Resolution
Medium exclude() fail-open/fail-closed footguns exclude() now prepends a missing leading slash, and path_excluded ignores an empty/"/" prefix (fails closed) instead of disabling CSRF site-wide. + 2 tests (exclude_root_does_not_disable_csrf, exclude_without_leading_slash_still_matches).
Medium is_valid() public → double-submit bypass if misused Now pub(crate). verify is the only public validation entry point.
Low Tower poll_ready/clone call() now drives the readied instance via std::mem::replace, leaving a clone for next time.
Low 419 has no Content-Type Sets text/plain; charset=utf-8.
Low .mime() invalid value panics Invalid override now falls back to application/octet-stream instead of panicking the builder. + test.
Low Unconditional immutable cache Documented on EmbeddedAssets that it assumes content-hashed filenames (behavior unchanged — by design).

Verification: cargo test --all-features green (60 lib + 11 csrf + 6 embed + 6 axum + 1 conformance + 26 spec-coverage + 3 doctests), cargo clippy --all-features --all-targets -- -D warnings clean, default no-features build/test unaffected, example builds.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/adapters/axum/embed.rs (1)

124-126: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Run cargo fmt to fix CI formatting failures.

Pipeline reports formatting differences in the status function. The exact diff isn't shown, but CI consistently flags this range across multiple runs.

#!/bin/bash
# Check what rustfmt wants to change
cd src/adapters/axum && rustfmt --check embed.rs 2>&1 || true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/axum/embed.rs` around lines 124 - 126, The formatting of the
status function (fn status(code: StatusCode) -> Response<Body>) is off and
failing CI; run rustfmt/cargo fmt or manually reformat that function so it
matches rustfmt style (fix spacing, indentation and line breaks around
Response::builder().status(code).body(Body::empty()).unwrap()), then commit the
formatted changes so the embed.rs file passes cargo fmt checks.
♻️ Duplicate comments (2)
tests/embed.rs (2)

76-79: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Run cargo fmt to fix CI formatting failures.

Pipeline reports formatting differences in the EmbeddedAssets::new closure and .mime() chaining.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/embed.rs` around lines 76 - 79, CI detected rustfmt/style issues in the
tests where the EmbeddedAssets::new closure and the chained .mime("custom", ...)
call are not formatted; run `cargo fmt` (or apply rustfmt) to reformat the file
so the closure passed to EmbeddedAssets::new and the subsequent .mime(...)
chaining follow the project's rustfmt rules, ensuring the closure body and
method chain are line-wrapped/indented per rustfmt for EmbeddedAssets::new and
the .mime method.

32-37: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Run cargo fmt to fix CI formatting failures.

Pipeline reports formatting differences in the assertion formatting. The assert_eq! calls with resp.headers().get(...) need reformatting to match rustfmt style.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/embed.rs` around lines 32 - 37, The assertions using assert_eq! with
resp.headers().get(...).unwrap() are not formatted to rustfmt style; run cargo
fmt and update the three assert_eq! calls so each expression follows rustfmt
formatting (e.g., keep short assertions on one line or let rustfmt reflow them)
— target the assert_eq! macros that reference resp, resp.headers().get(...), and
resp.status() to ensure they match CI formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/adapters/axum/embed.rs`:
- Around line 124-126: The formatting of the status function (fn status(code:
StatusCode) -> Response<Body>) is off and failing CI; run rustfmt/cargo fmt or
manually reformat that function so it matches rustfmt style (fix spacing,
indentation and line breaks around
Response::builder().status(code).body(Body::empty()).unwrap()), then commit the
formatted changes so the embed.rs file passes cargo fmt checks.

---

Duplicate comments:
In `@tests/embed.rs`:
- Around line 76-79: CI detected rustfmt/style issues in the tests where the
EmbeddedAssets::new closure and the chained .mime("custom", ...) call are not
formatted; run `cargo fmt` (or apply rustfmt) to reformat the file so the
closure passed to EmbeddedAssets::new and the subsequent .mime(...) chaining
follow the project's rustfmt rules, ensuring the closure body and method chain
are line-wrapped/indented per rustfmt for EmbeddedAssets::new and the .mime
method.
- Around line 32-37: The assertions using assert_eq! with
resp.headers().get(...).unwrap() are not formatted to rustfmt style; run cargo
fmt and update the three assert_eq! calls so each expression follows rustfmt
formatting (e.g., keep short assertions on one line or let rustfmt reflow them)
— target the assert_eq! macros that reference resp, resp.headers().get(...), and
resp.status() to ensure they match CI formatting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 93511bc5-2e4c-4986-8aee-be02fa21439a

📥 Commits

Reviewing files that changed from the base of the PR and between 03ebd1f and 67ce8f8.

📒 Files selected for processing (5)
  • src/adapters/axum/csrf.rs
  • src/adapters/axum/embed.rs
  • src/csrf/mod.rs
  • tests/csrf.rs
  • tests/embed.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
Cargo.toml (1)

51-51: Update getrandom dependency check (Cargo.toml)

  • getrandom 0.3 is not current: crates.io latest stable is 0.4.2 (latest 0.3.x is 0.3.4).
  • No known security vulnerabilities were reported by OSV for getrandom 0.3.4 (vulns=null), so the main action is currency (consider bumping to a newer compatible getrandom version).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Cargo.toml` at line 51, The Cargo.toml lists getrandom = { version = "0.3",
optional = true } which is out of date; update the dependency to a current
compatible version (for example "0.4.2") while preserving the optional = true
flag and any features or dependency targets that rely on getrandom; locate the
getrandom entry in Cargo.toml and change its version string to the newer
release, then run cargo update / cargo check to verify no downstream API or
feature breakage in functions/modules that depend on getrandom.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Cargo.toml`:
- Line 51: The Cargo.toml lists getrandom = { version = "0.3", optional = true }
which is out of date; update the dependency to a current compatible version (for
example "0.4.2") while preserving the optional = true flag and any features or
dependency targets that rely on getrandom; locate the getrandom entry in
Cargo.toml and change its version string to the newer release, then run cargo
update / cargo check to verify no downstream API or feature breakage in
functions/modules that depend on getrandom.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2e6320db-28c9-45df-ae0f-ed12b2bd1eac

📥 Commits

Reviewing files that changed from the base of the PR and between 67ce8f8 and 0c005af.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • Cargo.toml
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

- default-run = "axum-react-todo" so `cargo run` / `just dev` aren't
  ambiguous with the gen-bindings binary
- /todos/:id -> /todos/{id}: axum 0.8 rejects the 0.7 colon syntax at
  runtime (panic on router build); the server now boots

Both pre-existing, unrelated to the csrf/embed features.
The ts-bindings codegen extracted route params with the axum 0.7 colon/star
syntax (:id, *rest), so under the axum 0.8 dependency a {id} route yielded a
param-less URL helper. Add a shared segment_param() helper used by both
parse_params() and write_url_template(), so the type signature and the URL
template stay in sync, and cover it with a unit test.
SSR mode runs the backend with ssr_required(true). just dev launched the
Bun SSR sidecar and cargo run in parallel, so on a warm build the backend
could take the first request before the sidecar finished booting and return
'ssr transport: error sending request'. Gate the backend start on the
sidecar's /health endpoint.
@adiologydev adiologydev merged commit 8a4adbe into main May 26, 2026
3 of 4 checks passed
@adiologydev adiologydev deleted the feat/csrf-and-embedded-assets branch May 26, 2026 06:45
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.

1 participant