Skip to content

fix(transcode): route custom verbs after a path variable - #142

Merged
polaz merged 1 commit into
mainfrom
feat/#138
Oct 9, 2026
Merged

polaz merged 1 commit into
mainfrom
feat/#138

Conversation

@polaz

@polaz polaz commented Oct 9, 2026

Copy link
Copy Markdown
Member

Summary

  • A google.api.http template whose last segment is a variable followed by a custom verb (post: "/v2/ops/{operation}:cancel", AIP-136) is served instead of panicking the router at startup.
  • Bindings that differ only by verb reach their own RPCs; a template the router cannot match is skipped with an error in the log instead of a panic.
  • The request path of the proxy service does no work per request that can be done once: a 405 through it costs about a third of what it did, a transcoded call a little over half, and the number of routes does not change either.
  • The repository gains a contributing guide, a security policy and the organisation CLA note.

Replaces #141 (and #139 before it), squashed into one commit on top of main.

Changes

  • transcode/path.rs: template conversion moves here; a verb after a variable or wildcard is split off the mounted path, a verb after a literal stays in it. A template of one segment ({name=foo}) is a plain capture; a trailing **, alone or ending a field template, may match no segment before a verb. Templates are checked with matchit (the router axum uses) before registration.
  • transcode/table.rs: bindings of one path shape share one route, each with its own capture names. A request's verb is its last segment from the first unencoded : (neither a variable's value nor a verb holds one, google/api/http.proto). A verb some binding of the path binds owns the URL and only its bindings answer (escapes compared case-insensitively); an unbound verb stays part of the last variable, as in Envoy and grpc-gateway. A template ending in a literal keeps its exact URL. Verbs, and the bindings without one, are indexed in a segment trie that ranks paths as matchit does, so neither is hidden by a better-ranked path and the work per request is bounded by its path; an encoded verb is taken off the decoded value. A URL no binding answers is no transcoded request and goes to the proxy's other routes, then to the fallback (or 404), preflight included; one answered for other methods is 405 with them in Allow. The body is not read before that decision.
  • The transcoded paths are ranked among the paths of every other route as one router would: a request goes to its best match, and one a binding refuses passes on in that order, so another route ranked before a lower transcoded path takes it. A transcoded HEAD binding answers HEAD before another route's GET there, and Allow on a 405 lists the methods of the other routes at the path that answered too. The proxy service serves them past the router, behind the same layers and guards; router() keeps them in an axum router for embedding, choosing the binding after it routes. Captures are told from those of a prefix the router is nested under by position.
  • transcode/mod.rs: one dispatcher per route replaces the per-method router; path parameters are borrowed rather than copied; route_paths lists each route the router mounts, under the spelling it is mounted with (a path with verbs as *, since it answers every method), and both sides of a real clash (one method or a * rule, one verb), so ProxyServer refuses such a clash, an extra route on a path with verbs, or paths the router cannot hold together, at startup instead of panicking. Extra routes of a method the router skips, and transcoded routes it does not mount, reserve no path and lend no method to guard scopes.
  • A variable bound to a multi-segment template ({name=shelves/*/books/*}) takes only a value of that shape: at the end of a path it is checked past the router's catch-all, before it the template is written into the router path segment by segment; ** before the last segment is left out with an error. A variable over several segments, {name=**} included, keeps %2F encoded in its value (google/api/http.proto); a route with more variables than the router holds is refused rather than panicking it.
  • Layers: tracing, CORS and the client-address resolution wrap a route as one stack; the guards decided as a request arrives (a required client address, maintenance, concurrency, rate limits keyed before auth) run as one layer. CORS is the proxy's own, with its header values built at startup; only a real preflight (Fetch §3.2.2, with Access-Control-Request-Method) is answered by it, for REST and gRPC-Web alike, and for every origin it names back the request headers asked for, since * does not cover Authorization. Tracing wraps CORS, so a preflight is traced. A transcoded answer served past the router carries no Content-Length on a 204, a 304 or a 2xx CONNECT answer, which has no content either (RFC 9110 §8.6, §9.3.6).
  • Rate limits key and report a limit without allocating; trace ids are drawn from a per-thread generator seeded once rather than a system call per request; the forwarded header names are parsed once (TranscodeState::forwarded_header_names, a provided method).
  • Guards take the methods of the transcoded bindings directly; a route answering every method (a custom * rule, the verify endpoint) lets a scope over its own traffic name any method. OpenAPI documents only the routes the router mounts.
  • README documents custom verbs and the permissive CORS policy; ProxyService::with_fallback hands it a URL whose path a transcoded route matches but no binding answers.
  • CI pulls its Redis service through Google's Docker Hub mirror and runs the released cargo-deny binary, not its Docker action: anonymous Docker Hub pulls on shared runners hit its rate limit.
  • CONTRIBUTING.md, SECURITY.md, CLA line in the README license section; two broken private rustdoc links fixed.

Testing

Formatting, clippy and nextest for the default, no-backend and all-features builds, doc tests, rustdoc with warnings denied, the advisory audit and the MSRV check pass locally on macOS; CI runs the full matrix including musl.

Closes #138

- A `google.api.http` template whose last segment is a variable followed
  by a custom verb (`post: "/v2/ops/{operation}:cancel"`, AIP-136) is
  served instead of panicking the router at startup; bindings that
  differ only by verb reach their own RPCs, and a template the router
  cannot match is skipped with an error in the log
- Bindings of one path shape share one route; a verb some binding of
  the path binds owns the URL, an unbound verb stays part of the last
  variable, a template ending in a literal keeps its exact URL. Verbs
  and plain bindings are indexed in a segment trie that ranks paths as
  the router does
- The transcoded paths are ranked among every other route of the
  proxy: a request goes to its best match, one a binding refuses passes
  on in that order, a transcoded HEAD binding answers HEAD before
  another route's GET, and a 405 lists the methods of every route at
  the path that answered
- Startup refuses routes the router cannot hold together, a clash of
  one method or verb, and an extra route on a path with verbs; only
  mounted routes reserve paths or lend guard methods
- Multi-segment field templates bind only values of their shape and
  keep `%2F` encoded; a trailing `**` may match no segment before a
  verb; captures are told from a nesting prefix's by position
- The proxy service serves the transcoded routes past the router behind
  the same layers and guards; per-request work done once is done once
  (forwarded header names, trace ids, rate-limit keys, path parameters
  borrowed)
- CORS is the proxy's own and answers only real preflights; for every
  origin it names back the request headers asked for. No Content-Length
  on 204, 304 or a 2xx CONNECT answer
- CI pulls its Redis service through a Docker Hub mirror and runs the
  released cargo-deny binary
- CONTRIBUTING.md, SECURITY.md and the CLA note

Closes #138
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T21:23:09.655100Z 9248f71 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 20 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 12d099b1-1056-4eb6-aa3f-f96001becfc9

📥 Commits

Reviewing files that changed from the base of the PR and between 45a728d and 9248f71.


📒 Files selected for processing (35)
  • .github/workflows/ci.yml
  • CONTRIBUTING.md
  • Cargo.toml
  • README.md
  • SECURITY.md
  • src/client_address.rs
  • src/cors.rs
  • src/cors/tests.rs
  • src/embed.rs
  • src/guard.rs
  • src/guard/concurrency.rs
  • src/guard/gate.rs
  • src/guard/tests.rs
  • src/lib.rs
  • src/openapi.rs
  • src/openapi/tests.rs
  • src/service.rs
  • src/service/tests.rs
  • src/shield/matcher.rs
  • src/shield/mod.rs
  • src/shield/store.rs
  • src/shield/tests.rs
  • src/transcode/metadata.rs
  • src/transcode/metadata/tests.rs
  • src/transcode/mod.rs
  • src/transcode/path.rs
  • src/transcode/request.rs
  • src/transcode/table.rs
  • src/transcode/table/tests.rs
  • src/transcode/tests.rs
  • src/upstream.rs
  • src/upstream/tests.rs
  • tests/custom_verbs.rs
  • tests/hooks.rs
  • tests/upstream_controls.rs


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@polaz
polaz enabled auto-merge (squash) October 9, 2026 21:26
@polaz
polaz merged commit 2b06c27 into main Oct 9, 2026
10 checks passed
@polaz
polaz deleted the feat/#138 branch October 9, 2026 21:28
@sw-release-bot sw-release-bot Bot mentioned this pull request Oct 9, 2026
polaz pushed a commit that referenced this pull request Oct 9, 2026
## 🤖 New release

* `structured-proxy`: 8.1.0 -> 8.1.1 (✓ API compatible changes)

<details><summary><i><b>Changelog</b></i></summary><p>

<blockquote>

##
[8.1.1](v8.1.0...v8.1.1)
- 2026-10-09

### Fixed

- *(transcode)* route custom verbs after a path variable
([#142](#142))

### Other

- *(deps)* bump taiki-e/install-action from 2.87.21 to 2.87.24
([#137](#137))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

Co-authored-by: sw-release-bot[bot] <255865126+sw-release-bot[bot]@users.noreply.github.com>
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.

Support custom verbs after a path variable in google.api.http templates

1 participant