Skip to content

fix(minibf): answer unmatched paths with Blockfrost's global 400 - #1349

Merged
scarmuega merged 6 commits into
txpipe:mainfrom
arbelonson-source:fix/1323-minibf-invalid-path-fallback
Oct 1, 2026
Merged

scarmuega merged 6 commits into
txpipe:mainfrom
arbelonson-source:fix/1323-minibf-invalid-path-fallback

Conversation

@arbelonson-source

@arbelonson-source arbelonson-source commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Disclosure: this PR was written and opened by an autonomous coding agent (Claude Fable 5.1, via Claude Code) working on Arbel's account (arbelonson-source), taking the agent-friendly label on #1323 as the invitation. Please review it as agent output; if you'd rather not have agent contributions here, say so and I'll close it.

Closes #1323.

Summary

Every path that matched no route got an empty-body 404. Blockfrost (ryo's notFoundHandler) answers those with a global 400:

{"error":"Bad Request","message":"Invalid path. Please check https://docs.blockfrost.io/","status_code":400}

This adds a router-wide .fallback() that returns that body. The text lives in a new Error::InvalidPath variant, next to the other Blockfrost error bodies in error.rs, and the handler is a two-line routes::invalid_path.

Semantics

URL Before Now
/nonexistent, /foo/bar/baz, /txs, /blocks/latest/nope, /blocks/epoch/2/slot 404, empty body 400 Invalid path
/blocks/slot/ 404, empty body 400 Invalid path (the trailing slash is trimmed first, then nothing matches)
A matched route with a missing item, e.g. /blocks/{unknown hash} 404 404, unchanged

On the issue's open question: trailing-slash and empty-segment handling is left to the existing NormalizePathLayer plus this fallback. /blocks/latest/ still resolves to /blocks/latest as before, and whatever does not resolve gets the 400. A wrong method on a matched path (405) is not touched.

Testing

  • Two tests in crates/minibf/src/lib.rs: the six URLs from the issue's table all get the 400 with the exact body, and /blocks/{missing hash} still gets its 404, so the fallback does not swallow handler not-founds.
  • Reverted check: with the .fallback() line removed, the new test fails with unexpected status 404 Not Found for /nonexistent and an empty body, which is the behavior the issue reports.
  • cargo test -p dolos-minibf: 461 pass. cargo +nightly-2026-08-27 fmt --all -- --check and cargo clippy -p dolos-minibf --all-targets --all-features -- -D warnings are clean.

Summary by CodeRabbit

  • Bug Fixes
    • Unmatched paths now return a structured HTTP 400 “Bad Request” response with the message “Invalid path.” instead of an empty 404 response.
    • Requests using unsupported HTTP methods now receive the same consistent invalid-path response.
    • Trailing slashes are normalized before routing for more consistent route matching.

Every path that matched no route got an empty-body 404. Blockfrost
answers those with a 400 and the "Invalid path" body, so a client could
not tell a wrong URL from a missing component. A router-wide fallback
now returns that body for unknown endpoints, missing segments and empty
trailing segments alike; handler 404s are untouched.

Closes txpipe#1323.
@arbelonson-source
arbelonson-source requested review from a team and scarmuega as code owners September 15, 2026 16:30
@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 42f02b43-4321-49e0-9297-a91c0f0a09d4

📥 Commits

Reviewing files that changed from the base of the PR and between a04ea0b and ee20861.

📒 Files selected for processing (1)
  • crates/minibf/src/lib.rs

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

The minibf router now handles unmatched paths with a Blockfrost-style 400 response. Matched routes that cannot find a requested resource continue to return 404. Tests cover both behaviors.

Changes

Invalid path fallback

Layer / File(s) Summary
Invalid path error response
crates/minibf/src/error.rs
Adds Error::InvalidPath and converts it to a structured HTTP 400 response with the Blockfrost documentation message.
Router fallback and validation
crates/minibf/src/routes/mod.rs, crates/minibf/src/lib.rs
Registers routes::invalid_path as the router fallback. Tests verify 400 responses for unmatched paths and 404 responses for missing resources on matched routes.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: gonzalezzfelipe

Merge Risk: ⚪ Minimal · up to ee208

No actionable issue remains in the supplied evidence; the change is mergeable after normal checks.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #1323 requires the complete Blockfrost JSON response, including message Invalid path. Please check https://docs.blockfrost.io/. The current Error::InvalidPath uses Invalid path. only. The … Use the complete Blockfrost message in Error::InvalidPath. Add or update an automated test that verifies the complete JSON response.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: unmatched paths now return Blockfrost's global 400 response.
Out of Scope Changes check ✅ Passed The InvalidPath error, router-wide fallback, path normalization, wrong-method handling, and regression tests directly support issue #1323. No unrelated change is shown in the supplied evidence.
Full details: Linked Issues check

Explanation

Issue #1323 requires the complete Blockfrost JSON response, including message Invalid path. Please check https://docs.blockfrost.io/. The current Error::InvalidPath uses Invalid path. only. The fallback routing, trailing-slash normalization, wrong-method 405 behavior, matched-route 404 behavior, and regression tests address the other stated requirements.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Warning

Some tools did not complete. Review the errors below.

🔧 ast-grep (0.45.3)
crates/minibf/src/lib.rs

ast-grep timed out on this file


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.

@michalrus
michalrus requested review from slowbackspace and a lite review from Copilot September 15, 2026 17:58
@michalrus michalrus added area:minibf Mini Blockfrost (minibf) API bug Something isn't working labels Sep 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

/blocks/slot/ does not return the intended global 400 response and needs explicit handling.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Updates MiniBF to return Blockfrost-compatible 400 Invalid path responses for unmatched routes while preserving 404 responses for missing resources.

Changes:

  • Adds the InvalidPath error response.
  • Registers a router-wide fallback handler.
  • Adds tests for unmatched paths and preserved 404 behavior.
File summaries
File Summary
crates/minibf/src/routes/mod.rs Defines the invalid-path fallback handler.
crates/minibf/src/lib.rs Registers the fallback and adds tests; /blocks/slot/ still bypasses the fallback due to route matching.
crates/minibf/src/error.rs Implements the Blockfrost-compatible error body.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/minibf/src/lib.rs

@slowbackspace slowbackspace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks, the fallback itself is small and correct. One real problem hides behind it, plus a few test points.

Severity legend: 🔴 must fix before merge · 🟡 should fix · 🟢 nit / optional

Comment thread crates/minibf/src/lib.rs
"/governance/proposals/{gov_action_id}/parameters",
get(routes::governance::proposal_parameters_by_gov_action::<D>),
)
.fallback(routes::invalid_path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Trailing slashes are not trimmed before routing. NormalizePathLayer is added with Router::layer at line 710. Axum runs that layer after route matching, so the trim never affects which route matches. Before this PR, /blocks/latest/ returned an empty 404. With this fallback it returns 400 "Invalid path" for a valid endpoint. Blockfrost returns 200. The description claim that /blocks/latest/ still resolves is not what happens.

Fix: wrap the router instead of layering it, NormalizePathLayer::trim_trailing_slash().layer(app), and return that service. Commit c985eab did exactly this before #860 moved it to Router::layer. The ServiceExt::<Request>::into_make_service(app) call at line 732 is the leftover of that wrapping and already fits the wrapped type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right, and thanks — I had this wrong in my earlier note. Fixed in 9293b5a: the router is now wrapped with NormalizePathLayer::trim_trailing_slash().layer(app) instead of Router::layer, so trimming runs before route matching. /blocks/latest/ returns 200 (matching Blockfrost), /blocks/slot/ normalizes into /blocks/{hash_or_number} → 400 "Missing or malformed block hash.", and only genuinely unmatched paths reach the fallback. ServiceExt::<Request>::into_make_service at the serve call already fits the wrapped type, as you noted. Added trailing_slash_is_trimmed_before_routing as a regression guard.

— (posted by Arbel's coding agent, Claude via Claude Code, same as the PR)

Comment thread crates/minibf/src/lib.rs
"/governance/proposals/{gov_action_id}/parameters",
get(routes::governance::proposal_parameters_by_gov_action::<D>),
)
.fallback(routes::invalid_path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Wrong method still gets an empty 405. Blockfrost answers POST /blocks/latest with the same "Invalid path" 400. One line closes it:

.method_not_allowed_fallback(routes::invalid_path)

The description marks this as out of scope, so treat it as optional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Included it in 9293b5a — .method_not_allowed_fallback(routes::invalid_path), so POST /blocks/latest now returns the same 400 "Invalid path." body as an unknown route, with a wrong_method_answers_with_blockfrost_invalid_path test. Happy to drop it if you'd rather keep this PR to the original scope.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

drop it. 405 sounds good to me in this case even if it doesn't match blockfrost.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dropped in 9716b98 — removed method_not_allowed_fallback and its test, so a wrong method is back to the default 405. Agreed, keeping it simple.

Comment thread crates/minibf/src/lib.rs Outdated
"/txs",
"/blocks/latest/nope",
"/blocks/epoch/2/slot",
"/blocks/slot/",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 This row encodes the layer-ordering bug. Once trimming runs before routing, /blocks/slot/ matches /blocks/{hash_or_number} and returns 400 "Missing or malformed block hash." That is also what Blockfrost returns. The issue table lists only a bare 400 for this row, not the "Invalid path" body.

Drop the row, or assert the block-hash body for it. Also add a positive check that /blocks/latest/ returns 200, so the trailing-slash behavior has a regression guard.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — dropped /blocks/slot/ from UNMATCHED_PATHS (it's a matched route now), and the new trailing_slash_is_trimmed_before_routing test asserts both the /blocks/latest/ 200 and the /blocks/slot/ block-hash 400.

Comment thread crates/minibf/src/lib.rs Outdated

@slowbackspace slowbackspace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One more point on the error text.

Comment thread crates/minibf/src/error.rs Outdated
Wrap the router with NormalizePathLayer instead of adding it via
Router::layer, so the normalizer runs before route matching rather than
after it (restoring the pre-txpipe#860 wrapping). `/blocks/latest/` now
resolves to 200 as Blockfrost does, instead of falling through to the
invalid-path body, and `/blocks/slot/` normalizes into the
`/blocks/{hash_or_number}` route's 400 "Missing or malformed block hash."

Also answer a wrong method with the same invalid-path 400 via
method_not_allowed_fallback (Blockfrost does the same), drop the
Blockfrost docs link from the invalid-path message, remove a not-found
test that duplicated blocks_by_hash_or_number_not_found, and add
trailing-slash and wrong-method regression tests.
@arbelonson-source

Copy link
Copy Markdown
Contributor Author

Pushed 9293b5a addressing the review — thanks @slowbackspace, the layer-ordering catch was the real one and I had it wrong in my earlier note.

  • 🔴 Trailing slashes — wrapped the router (NormalizePathLayer::trim_trailing_slash().layer(app)) so trimming runs before routing. /blocks/latest/ → 200 (Blockfrost parity), /blocks/slot/ → the /blocks/{hash_or_number} route's 400 "Missing or malformed block hash.", and only genuinely unmatched paths reach the fallback.
  • 🟡 Wrong method — added .method_not_allowed_fallback(routes::invalid_path); POST /blocks/latest now returns the invalid-path 400. (Optional per scope — easy to drop if you'd prefer.)
  • 🟡 Test row — dropped /blocks/slot/ from the unmatched list; added a trailing_slash_is_trimmed_before_routing regression test asserting the /blocks/latest/ 200 and /blocks/slot/ 400, plus a wrong-method test.
  • 🟢 Duplicate test — removed; blocks_by_hash_or_number_not_found already covers it.
  • 🟡 Docs link — message is now just Invalid path.

All 461 minibf tests pass; cargo clippy -p dolos-minibf --all-targets -- -D warnings and cargo +nightly-2026-08-27 fmt --all -- --check are clean. Ready for another look when you have a moment.

Comment thread crates/minibf/src/lib.rs Outdated
Apply NormalizePath behind an outer Router via `fallback_service`
instead of returning `NormalizePath<Router>` from the public
`build_router`, so the wrapped type stays an implementation detail and
the public signature is unchanged. Same trim-before-routing behaviour;
all tests still pass.
Drop method_not_allowed_fallback and its test per review: a wrong method
now returns axum's default 405 rather than the invalid-path 400. This
diverges from Blockfrost there, which is the intended behaviour.
@arbelonson-source

Copy link
Copy Markdown
Contributor Author

Disclosure: I'm an autonomous coding agent (Claude Opus 5.5, via Claude Code) on Arbel's account.

@michalrus — your Router point is in a04ea0b (build_router returns Router again, normaliser nested via fallback_service), and @slowbackspace's 405 note is in 9716b98. I re-checked against today's main after #1354 landed: it merges without conflicts, and on the merged tree cargo test -p dolos-minibf is 489 passed / 0 failed with clippy -D warnings clean. Ready for another look whenever you have time.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The response body and /blocks/slot/ behavior contradict the issue’s required Blockfrost semantics.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
Resolved since last review (1)

Comment thread crates/minibf/src/error.rs
Comment thread crates/minibf/src/lib.rs
@michalrus
michalrus requested review from michalrus and a balanced review from Copilot September 23, 2026 13:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The implementation matches the clarified expected behavior and includes focused regression coverage.

Review effort: Balanced
Findings: None

Issue txpipe#1323 lists empty path segments as one of the cases that must get
the global 400. The unmatched-path test did not cover this case. Add
`/blocks//latest` to `UNMATCHED_PATHS`.

`NormalizePathLayer` removes only the slashes at the start and at the
end of a path. As a result, the router receives `/blocks//latest`
unchanged, and the fallback answers with the invalid-path body.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:minibf Mini Blockfrost (minibf) API bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

minibf: unmatched paths return empty 404, Blockfrost returns global 400 "Invalid path"

5 participants