Skip to content

Add routing rules to the YARP application#3022

Draft
davidfowl wants to merge 12 commits intomainfrom
app-routing-rules
Draft

Add routing rules to the YARP application#3022
davidfowl wants to merge 12 commits intomainfrom
app-routing-rules

Conversation

@davidfowl
Copy link
Copy Markdown
Member

Summary

Adds routing-rule features to the YARP application static-hosting experience so the pre-built app can cover common static site, SPA, edge frontend, and simple reverse-proxy composition scenarios without writing code.

This includes:

  • Headers[] for static-host responses
  • Redirects[] as routed short-circuit endpoints
  • NavigationFallback.Exclude[] for paths that should not fall back to the SPA shell
  • Rewrites[] using the ASP.NET Core rewrite middleware syntax
  • ErrorPages for custom exact-code and Nxx wildcard error pages
  • documentation for the request pipeline mental model
  • realistic sample app configs under samples/YarpApplication.SampleApps

Mental model

The app is composed as a small request pipeline. Each feature has a clear position and responsibility:

1. Rewrites             mutate request path before routing
2. ErrorPages           wrap downstream 4xx/5xx responses and re-execute custom pages
3. Routing              select endpoints
4. Redirects            routed endpoints that short-circuit
5. Static files         serve existing files before proxy/fallback endpoints
6. Headers              mutate static-host responses only
7. Reverse proxy        proxy matched backend routes
8. Fallback exclusions  routed 404 endpoints for excluded SPA paths
9. SPA fallback         serve NavigationFallback.Path for remaining non-file routes

Important consequences:

  • Rewrites run first, so every downstream feature sees the rewritten path.
  • Redirects are endpoints, so they participate in routing and win before static/proxy/fallback behavior.
  • Static files are special: the app preserves their historical precedence even though routing runs earlier, so existing files still win over catch-all proxy/fallback endpoints.
  • Headers are static-host-only: they apply to static files and SPA fallback responses, but not proxied responses.
  • Fallback exclusions are endpoint-routed 404s, which means excluded paths still compose with routing and error pages.
  • Error pages wrap downstream behavior and re-execute the configured page while preserving the original status code.

Matching model

There are two intentional matching syntaxes.

Routed features use ASP.NET route templates

This applies to:

  • Headers[].Match.Path
  • Redirects[].Match.Path
  • NavigationFallback.Exclude[].Path

Examples:

{ "Path": "/api/{**catch-all}" }
{ "Path": "/docs/{slug}" }

Redirects[].Destination can reference route values captured from the match:

{
  "Match": { "Path": "/docs/{**slug}" },
  "Destination": "/articles/{slug}",
  "StatusCode": 301
}

Rewrites use ASP.NET Core rewrite middleware syntax

No new rewrite DSL is invented. Rewrites use regex + replacement capture groups:

{
  "Regex": "^legacy/(.*)$",
  "Replacement": "api/$1"
}

SkipRemainingRules defaults to true, matching “first rewrite wins” behavior.

Error pages

ErrorPages maps status codes to custom pages:

{
  "ErrorPages": {
    "404": "/errors/not-found.html",
    "5xx": "/errors/server-error.html"
  }
}

Rules support:

  • exact codes: "404", "503"
  • class wildcards: "4xx", "5xx"
  • exact code wins over wildcard

The implementation re-executes the request against the configured page, clears/resets response state so routed/proxied targets can run, and preserves the original HTTP status code before the response is sent.

Samples

Adds five config/static-file sample apps:

Sample Demonstrates
01-marketing-site Static site, SPA-style fallback, cache/security headers
02-docs-site Legacy redirects, current-version rewrites, docs headers
03-dashboard-spa SPA fallback plus /api proxy exclusion
04-commerce-errors Branded exact and wildcard error pages
05-edge-composition Rewrites, redirects, static assets, proxying, fallback exclusions, and error pages together

These are intentionally not .NET apps; they are realistic appsettings.json + wwwroot layouts for the YARP application.

Validation

  • dotnet test test/Application.Tests/Yarp.Application.Tests.csproj
  • Manual smoke of each sample config to confirm the app starts and representative routes work
  • Code review pass found no significant issues

davidfowl and others added 9 commits April 27, 2026 20:14
Routes-first design for the YARP container app's static-host pipeline:

- Redirects -> Map(...).ShortCircuit() routed endpoints (early order).
- NavigationFallback.Exclude -> MapFallback(...) endpoints ordered just
  ahead of the SPA fallback so proxy and other real routes still win.
- Headers -> middleware that targets static-file responses
  (OnPrepareResponse) and the SPA fallback (OnStarting via endpoint
  metadata). Static files are not endpoints, so this remains the only
  consumer of RequestMatchEvaluator.TryMatch.
- StaticFilesFeature wraps UseFileServer to stash/clear and restore the
  selected endpoint, preserving the rule that static files beat routed
  fallback endpoints while routed real endpoints still win.
- RequestMatchEvaluator exposes a static ValidatePath helper so callers
  that delegate matching to ASP.NET routing skip the TemplateMatcher
  allocation entirely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Delegates to the standard ASP.NET rewrite middleware
(Microsoft.AspNetCore.Rewrite) instead of inventing a new syntax.
Config maps directly to RewriteOptions.AddRewrite parameters:

  { "Regex": "^old/(.*)$", "Replacement": "new/$1" }

Slots in before UseRouting so every downstream stage (route matching,
static files, redirects, SPA fallback, reverse proxy) sees the
rewritten path. SkipRemainingRules defaults to true (first match wins);
set false to chain rewrites.

7 new tests cover passthrough, capture-group substitution, ordering vs.
routing/redirects/proxy/fallback exclusions, and the no-chaining
default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- RedirectsFeature and NavigationFallbackExclusionsFeature captured the
  loop variable 'i' in the .Add() callback. Endpoint convention
  callbacks run after the loop completes, so all endpoints were getting
  the same final Order value. Hoist Order into a per-iteration local.
  Existing tests didn't catch this because each rule has a unique path,
  so Order ties weren't observable.
- Wrap RewriteOptions.AddRewrite in try/catch so an invalid Regex
  pattern surfaces as 'Rewrite rule at index N has an invalid Regex
  pattern "..."' instead of a generic ArgumentException from the
  Regex constructor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a 'Request pipeline' section to the application README that lays
out the eight stages (rewrites -> routing match -> redirects -> static
files -> headers -> reverse proxy -> fallback exclusions -> SPA
fallback) as the central mental model, plus a 'Match syntax' note
explaining why routed features use route templates while Rewrites use
regex (delegation to the standard rewrite middleware).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Maps HTTP status codes to custom response files via re-execute. Supports
exact codes ("404") and class wildcards ("5xx"); exact wins over wildcard.
Slots between Rewrites and Routing in the request pipeline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clear the response before re-executing a configured error page so routed and proxied targets can run normally, then restore the original status code before the response is sent. Add coverage for a proxied error page target that writes 200 OK while the client still receives the original 404.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add five realistic config/static-file sample apps that demonstrate the YARP application static-hosting and routing-rule features: marketing site headers, docs redirects/rewrites, dashboard API proxying, custom commerce error pages, and a composed edge frontend.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Normalize table spacing and remove an extra trailing blank line so markdownlint passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous Azure Ubuntu leg failed in ReverseProxy.FunctionalTests Expect100Continue coverage, unrelated to the YARP Application changes in this branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidfowl davidfowl requested a review from sebastienros April 29, 2026 04:38
davidfowl and others added 3 commits April 28, 2026 21:54
Add comments explaining why custom error pages clear response state, restore the original status code, and reset routing state during re-execute.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document the route-template, rewrite regex, redirect destination, and error-page key parsing paths with concrete examples so the config syntax is easier to follow in code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move relative endpoint order values to named constants and explain how endpoint routing uses those ranges for redirects and SPA fallback exclusions.

Co-authored-by: Copilot <223556219+Copilot@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.

1 participant