Skip to content

fix(transcode): emit axum 0.8 path syntax in proto_path_to_axum#18

Merged
polaz merged 5 commits into
mainfrom
fix/#17-axum08-path-syntax
Jun 19, 2026
Merged

fix(transcode): emit axum 0.8 path syntax in proto_path_to_axum#18
polaz merged 5 commits into
mainfrom
fix/#17-axum08-path-syntax

Conversation

@polaz

@polaz polaz commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

The 1.0.2 axum 0.7 → 0.8 bump left proto_path_to_axum emitting the old :param capture syntax. axum 0.8 rejects any path segment starting with : at Router::route(), so every google.api.http rule with a {param} template panicked the REST transcoding router at startup. gRPC was unaffected; the whole REST listener was dead.

Fix

axum 0.8's capture syntax matches the google.api.http {param} form, so plain params now pass through verbatim. Field-path / wildcard templates are rewritten:

proto template axum 0.8
{name} {name}
{name=*} {name}
{name=**} {*name} (catch-all)
bare * / ** position-named capture

Testing

  • Flipped test_proto_path_to_axum to assert 0.8 output.
  • Added test_proto_path_to_axum_wildcards covering field-path/catch-all/bare wildcards.
  • Added router_builds_with_brace_path_params_on_axum_0_8 — builds the real router over the brace-param path (the panic site) and asserts no panic. Pre-fix this panicked.
  • cargo nextest run: 35 passed. cargo clippy --all-targets: clean.

Release

semantic-release will cut 1.0.3 from this fix: commit. 1.0.2 is not yanked (it carries the jsonwebtoken 10.4.0 / GHSA-h395 security fix); 1.0.3 keeps that and repairs transcoding.

Closes #17

The 1.0.2 axum 0.7 -> 0.8 bump left proto_path_to_axum converting
proto `{id}` templates to the old `:id` capture syntax. axum 0.8
rejects any path segment starting with `:` at Router::route(), so
every brace-param http rule panicked the REST transcoding router at
startup ("Path segments must not start with `:`"). gRPC was fine; the
whole REST listener was dead.

axum 0.8's capture syntax IS the google.api.http `{param}` form, so
plain params now pass through verbatim. Field-path and wildcard
templates are rewritten correctly:
- `{name=*}`  -> `{name}`
- `{name=**}` -> `{*name}` (catch-all)
- bare `*` / `**` -> position-named captures

Flipped the unit test to assert 0.8 output and added a regression test
that builds the real router over a brace-param path (the panic site)
and asserts it does not panic.

Closes #17
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0a1b8c5c-dea7-4c1b-8b83-c8b0d4ca7af1

📥 Commits

Reviewing files that changed from the base of the PR and between ee657e8 and 10e038b.

📒 Files selected for processing (1)
  • src/transcode/mod.rs

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved REST→gRPC transcoding path-template conversion for the latest routing syntax, including correct handling of brace parameters, wildcards, and terminal catch-all routes.
    • Prevents unsafe splitting of brace templates that contain embedded / and safely degrades unsupported or non-terminal catch-all templates to keep routing deterministic.
  • Tests

    • Updated and expanded conversion tests to match the new brace-capture syntax, covering *, **, wildcard/catch-all behavior, and multi-segment brace templates.
    • Added a regression check to ensure router construction with brace-path templates does not panic.

Walkthrough

proto_path_to_axum in src/transcode/mod.rs is rewritten from a character-scanning :param emitter to a brace-depth-aware segment converter that produces Axum 0.8 brace-capture syntax. Segment rewrites handle plain {param} passthrough, {name=*} to {name} rewriting, {name=**} to {*name} catch-all conversion, and bare wildcard to position-named capture generation. Multi-segment field templates are detected, collapsed with warnings, and non-terminal catch-alls are degraded to prevent Axum router rejection. Unit tests are updated to assert the new brace output and expanded with wildcard, embedded-slash, and router-building regression coverage.

Changes

Axum 0.8 path conversion fix

Layer / File(s) Summary
proto_path_to_axum rewrite and test coverage
src/transcode/mod.rs
Replaces the :param character-scanner with a brace-depth-aware segment-based loop that passes {param} through verbatim, rewrites {name=*} to {name}, converts {name=**} to {*name} (catch-all), and converts bare */** to position-named captures ({wildcardN}/{*wildcardN}). Multi-segment field templates containing embedded slashes are detected and collapsed to catch-all with a warning. Non-terminal catch-all patterns are degraded to single-segment captures to avoid invalid mid-path {*name} in Axum routers. Unit test assertions are updated to expect brace-capture output, new cases cover wildcard templates, embedded-slash regressions, and a router-building regression test confirms Router::route() does not panic.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~23 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: updating proto_path_to_axum to emit axum 0.8 path syntax instead of the old 0.7 syntax.
Description check ✅ Passed The description thoroughly explains the critical bug, root cause, fix details, testing approach, and release strategy—all directly related to the changeset.
Linked Issues check ✅ Passed All acceptance criteria from issue #17 are met: proto_path_to_axum emits axum 0.8 syntax, wildcard/catch-all templates are rewritten correctly, unit tests are updated, regression test building the router is included, and path params are properly extracted.
Out of Scope Changes check ✅ Passed All changes directly address the scope of issue #17: fixing proto_path_to_axum to emit axum 0.8 syntax, handling multi-segment field templates, and adding regression tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 fix/#17-axum08-path-syntax

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

@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes the axum 0.7 → 0.8 migration bug in proto_path_to_axum: the old code emitted :param captures that axum 0.8 rejects at startup, causing every google.api.http-annotated REST route to panic. The new code passes {param} through verbatim (axum 0.8's native syntax), rewrites field-path templates ({name=*}, {name=**}), names bare wildcards by position, and degrades non-terminal catch-alls to single-segment captures with a tracing::warn.

  • split_top_level tracks brace depth to avoid fracturing multi-segment field templates like {name=shelves/*/books/*} when splitting on /.
  • Four new tests cover plain params, wildcards, non-terminal catch-all degradation, and the multi-segment regression; several tests construct a live Router to confirm axum 0.8 accepts the emitted paths without panicking.

Confidence Score: 5/5

The change is a targeted, well-tested fix to a function that previously panicked the entire REST listener on startup; no auth, persistence, or cross-service paths are touched.

The rewrite is logically correct across all handled cases, the brace-depth splitter properly handles multi-segment field templates, and the tests include live axum router construction that would catch any regression in the emitted path syntax.

No files require special attention.

Important Files Changed

Filename Overview
src/transcode/mod.rs Replaces the single-pass {…}→:param rewrite with a brace-depth-aware segment splitter and a proper axum 0.8 path emitter; adds four regression tests including live router construction.

Reviews (3): Last reviewed commit: "fix(transcode): degrade non-terminal cat..." | Re-trigger Greptile

Comment thread src/transcode/mod.rs
polaz added 2 commits June 19, 2026 01:12
google.api.http resource-name templates embed slashes inside a single
brace span, e.g. `{name=shelves/*/books/*}`. proto_path_to_axum splits
on `/` before inspecting brace captures, fracturing such a span into
invalid fragments and emitting a mangled axum path that panics at
Router::route(). This test reproduces the failure; the fix follows.

Part of #17
proto_path_to_axum split the path on `/` before inspecting brace
captures, so a google.api.http multi-segment field template like
`{name=shelves/*/books/*}` (slashes embedded in one capture) fractured
into invalid fragments and emitted a mangled axum path that panicked at
Router::route().

Split only at top-level `/` (tracking brace depth) so each capture
stays intact. Multi-segment field templates with interspersed literals
have no faithful axum representation, so collapse them to a `{*name}`
catch-all and emit a tracing::warn, surfacing the limitation instead of
silently mis-routing. Single-segment `{name=*}` / `{name=**}` and plain
`{name}` captures are unchanged.

Part of #17

@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/transcode/mod.rs (1)

468-473: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Non-terminal unsupported field templates can still panic router registration.

On Line 472 / Lines 521-528, unsupported {name=...} templates always become {*name}. If that segment is not last (e.g. /v1/{name=projects/*}/topics), the output becomes /v1/{*name}/topics, which axum rejects at Router::route().

Proposed fix
 pub fn proto_path_to_axum(path: &str) -> String {
     let mut out = String::with_capacity(path.len());
+    let segments = split_top_level(path);
 
-    for (idx, segment) in split_top_level(path).into_iter().enumerate() {
+    for (idx, segment) in segments.iter().enumerate() {
         if idx > 0 {
             out.push('/');
         }
-        out.push_str(&convert_segment(segment, idx));
+        let is_last = idx + 1 == segments.len();
+        out.push_str(&convert_segment(segment, idx, is_last));
     }
 
     out
 }
@@
-fn convert_segment(segment: &str, idx: usize) -> String {
+fn convert_segment(segment: &str, idx: usize, is_last: bool) -> String {
@@
                 _ => {
                     tracing::warn!(
                         template = %inner,
                         "google.api.http multi-segment field template is not fully \
                          supported; routing it as a catch-all capture"
                     );
-                    format!("{{*{name}}}")
+                    if is_last {
+                        format!("{{*{name}}}")
+                    } else {
+                        tracing::warn!(
+                            template = %inner,
+                            "non-terminal multi-segment template cannot be represented as axum catch-all; degrading to single-segment capture"
+                        );
+                        format!("{{{name}}}")
+                    }
                 }
             };
         }
+    #[test]
+    fn non_terminal_multi_segment_template_does_not_generate_mid_catch_all() {
+        assert_eq!(
+            proto_path_to_axum("/v1/{name=projects/*}/topics"),
+            "/v1/{name}/topics"
+        );
+        let path = proto_path_to_axum("/v1/{name=projects/*}/topics");
+        let _router: Router<()> = Router::new().route(&path, get(|| async { "ok" }));
+    }

Also applies to: 507-529

🤖 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/transcode/mod.rs` around lines 468 - 473, The convert_segment function
converts all unsupported field templates like {name=projects/*} to {*name}, but
catch-all patterns ({*name}) are only valid in the last path segment. In the
loop iterating through split_top_level results, pass information about whether
the current segment is terminal (the last one) to convert_segment by either
passing a boolean parameter indicating if this is the final segment or by
passing the total segment count. Then modify the convert_segment function to
check if an unsupported template is being processed in a non-terminal position
and either return an error or handle it differently instead of unconditionally
converting to the {*name} catch-all pattern which will cause axum
Router::route() to reject the route registration.
🤖 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/transcode/mod.rs`:
- Around line 468-473: The convert_segment function converts all unsupported
field templates like {name=projects/*} to {*name}, but catch-all patterns
({*name}) are only valid in the last path segment. In the loop iterating through
split_top_level results, pass information about whether the current segment is
terminal (the last one) to convert_segment by either passing a boolean parameter
indicating if this is the final segment or by passing the total segment count.
Then modify the convert_segment function to check if an unsupported template is
being processed in a non-terminal position and either return an error or handle
it differently instead of unconditionally converting to the {*name} catch-all
pattern which will cause axum Router::route() to reject the route registration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 24080bee-3587-45ef-8b04-e399d3b9c08c

📥 Commits

Reviewing files that changed from the base of the PR and between b7c0338 and ee657e8.

📒 Files selected for processing (1)
  • src/transcode/mod.rs

polaz added 2 commits June 19, 2026 10:31
axum permits a catch-all capture `{*name}` only in the LAST path
segment. convert_segment collapses unsupported / `**` field templates
to `{*name}` unconditionally, so a non-terminal template like
`/v1/{name=projects/*}/topics` produces `/v1/{*name}/topics`, which
axum rejects at Router::route(). This test reproduces it; fix follows.

Part of #17
convert_segment collapsed unsupported / `**` field templates to an axum
catch-all `{*name}` regardless of position. axum accepts a catch-all
only in the final path segment, so a non-terminal template such as
`/v1/{name=projects/*}/topics` produced `/v1/{*name}/topics` and
panicked the router at Router::route().

Thread `is_last` through convert_segment and route every catch-all
emission through a `catch_all` helper that produces `{*name}` only for
the terminal segment, otherwise degrades to a single-segment `{name}`
capture and warns. Single-segment and plain captures are unchanged.

Part of #17
@polaz
polaz merged commit 40dac57 into main Jun 19, 2026
6 checks passed
@sw-release-bot

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.0.3 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(transcode): REST router panics on axum 0.8 — proto_path_to_axum emits 0.7 :param syntax

1 participant