-
Notifications
You must be signed in to change notification settings - Fork 839
Description
Background and motivation
HTTP paths contain dynamic IDs that show up in logs, metrics, traces, cache keys and policy checks, yet those same IDs may be sensitive. Right now callers who want a clean, stable, privacy‑aware version of a request path either pull in the full ASP.NET Core routing stack or write their own fragile parsing and masking code. The existing internal HttpRouteParser
and HttpRouteFormatter
already solve this efficiently: the parser turns a route template into immutable segments (handling optional, default and catch‑all pieces); the formatter rebuilds the actual path into a canonical form, applying Strict, Loose or None redaction rules using data classifications and a redactor provider.
Making them public lets any telemetry, logging or security component use the exact same canonicalization and redaction logic the library relies on, avoiding duplicate implementations and inconsistent results. It also gives external code structured access to parameter names, default values and omission state without reinventing offset and span calculations. Their sealed, focused design and internal caching are ready for direct consumption; exposing an interface instead would force everyone to copy the parsing details, while opening extensibility by inheritance would add surface area and risk. Public access simply turns a proven internal utility into a shared building block for safe, uniform route handling.
API Proposal
namespace Microsoft.Extensions.Http.Diagnostics
{
/// <summary>
/// Parses HTTP route templates and extracts redacted parameter values from concrete request paths.
/// </summary>
public sealed class HttpRouteParser
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpRouteParser"/> class.
/// </summary>
/// <param name="redactorProvider">Provider used to obtain redactors for classified parameters.</param>
public HttpRouteParser(IRedactorProvider redactorProvider);
/// <summary>
/// Parses a route template into immutable text and parameter segments.
/// </summary>
/// <param name="httpRoute">The route template (e.g. "/api/items/{id?}").</param>
/// <returns>The parsed representation of the template.</returns>
public ParsedRouteSegments ParseRoute(string httpRoute);
/// <summary>
/// Attempts to extract parameter values from a concrete HTTP path using pre-parsed segments.
/// Values are redacted according to the specified mode and classification map. Caller supplies the destination buffer.
/// </summary>
/// <param name="httpPath">Concrete request path (e.g. "/api/items/123"). Leading slash is optional.</param>
/// <param name="routeSegments">Previously parsed route segments.</param>
/// <param name="redactionMode">Redaction strategy (Strict, Loose, None).</param>
/// <param name="parametersToRedact">Classification map keyed by parameter name.</param>
/// <param name="httpRouteParameters">
/// Buffer to receive extracted parameters. Must be non-null and have capacity.
/// </param>
/// <returns><see langword="true" /> if extraction succeeded; <see langword="false" /> if buffer was insufficient.</returns>
public bool TryExtractParameters(
string httpPath,
in ParsedRouteSegments routeSegments,
HttpRouteParameterRedactionMode redactionMode,
IReadOnlyDictionary<string, DataClassification> parametersToRedact,
ref HttpRouteParameter[] httpRouteParameters) => throw new NotImplementedException();
/// <summary>
/// Extracts parameter values and returns a new array. Prefer <see cref="TryExtractParameters"/> to avoid allocations under load.
/// </summary>
/// <param name="httpPath">Concrete request path.</param>
/// <param name="routeSegments">Parsed route segments.</param>
/// <param name="redactionMode">Redaction strategy.</param>
/// <param name="parametersToRedact">Classification map keyed by parameter name.</param>
/// <returns>Newly allocated array containing parameter values and redaction flags.</returns>
public HttpRouteParameter[] ExtractParameters(
string httpPath,
in ParsedRouteSegments routeSegments,
HttpRouteParameterRedactionMode redactionMode,
IReadOnlyDictionary<string, DataClassification> parametersToRedact);
}
/// <summary>
/// Immutable representation of a parsed route template and its segments.
/// </summary>
public readonly struct ParsedRouteSegments
{
/// <summary>
/// Initializes a new instance of the <see cref="ParsedRouteSegments"/> struct.
/// </summary>
/// <param name="routeTemplate">Original route template.</param>
/// <param name="segments">Ordered segments from the template.</param>
public ParsedRouteSegments(string routeTemplate, Segment[] segments);
/// <summary>Original route template, normalized (leading slash removed).</summary>
public string RouteTemplate { get; }
/// <summary>All segments (text and parameter) in original order.</summary>
public Segment[] Segments { get; }
/// <summary>Total count of parameter segments.</summary>
public int ParameterCount { get; }
}
/// <summary>
/// Describes a single segment of a parsed route template (text or parameter).
/// </summary>
public readonly struct Segment
{
/// <summary>
/// Initializes a new instance of the Segment struct.
/// </summary>
/// <param name="start">Start index in the normalized template.</param>
/// <param name="end">End index (exclusive) in the normalized template.</param>
/// <param name="content">Segment content (parameter content without braces, or raw text).</param>
/// <param name="isParam">Whether this segment represents a parameter.</param>
/// <param name="paramName">Parameter name (empty if <paramref name="isParam"/> is false).</param>
/// <param name="defaultValue">Default value if specified; otherwise empty.</param>
/// <param name="isCatchAll">True if the parameter is a catch-all (starts with * or **).</param>
public Segment(int start, int end, string content, bool isParam, string paramName = "", string defaultValue = "", bool isCatchAll = false);
/// <summary>Start index in the template.</summary>
public int Start { get; }
/// <summary>End index (exclusive) in the template.</summary>
public int End { get; }
/// <summary>Raw content of this segment.</summary>
public string Content { get; }
/// <summary>True if this segment is a parameter.</summary>
public bool IsParam { get; }
/// <summary>Parameter name if this is a parameter segment; otherwise empty.</summary>
public string ParamName { get; }
/// <summary>Default value specified for the parameter (may be empty).</summary>
public string DefaultValue { get; }
/// <summary>True if the parameter captures all remaining path segments.</summary>
public bool IsCatchAll { get; }
/// <summary>
/// Returns <see langword="true" /> if the provided parameter name is considered well-known and never redacted implicitly.
/// </summary>
/// <param name="parameter">The parameter name.</param>
public static bool IsKnownUnredactableParameter(string parameter);
}
/// <summary>
/// A parameter value extracted from an HTTP path along with its redaction state.
/// </summary>
public readonly struct HttpRouteParameter
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpRouteParameter"/> struct.
/// </summary>
/// <param name="name">Parameter name.</param>
/// <param name="value">Parameter value (possibly redacted).</param>
/// <param name="isRedacted">Whether the value was redacted.</param>
public HttpRouteParameter(string name, string value, bool isRedacted);
/// <summary>Parameter name.</summary>
public string Name { get; }
/// <summary>Parameter value (original or redacted placeholder).</summary>
public string Value { get; }
/// <summary>True if redaction was applied.</summary>
public bool IsRedacted { get; }
}
}
/// <summary>
/// Produces a canonical representation of an HTTP path with optional parameter redaction.
/// </summary>
/// <remarks>
/// Uses parsed route segments to rebuild the path deterministically, applying strict or loose
/// redaction rules based on provided classifications.
/// </remarks>
public sealed class HttpRouteFormatter
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpRouteFormatter"/> class.
/// </summary>
/// <param name="parser">Route template parser.</param>
/// <param name="redactorProvider">Provider for classification-based redactors.</param>
public HttpRouteFormatter(HttpRouteParser parser, IRedactorProvider redactorProvider);
/// <summary>
/// Formats a concrete path against a route template, redacting sensitive parameters.
/// </summary>
/// <param name="httpRoute">Route template used for parsing.</param>
/// <param name="httpPath">Concrete request path to format.</param>
/// <param name="redactionMode">Redaction strategy (Strict, Loose, None).</param>
/// <param name="parametersToRedact">Classification map keyed by parameter name.</param>
/// <returns>Canonical formatted path with redacted values as required.</returns>
public string Format(
string httpRoute,
string httpPath,
HttpRouteParameterRedactionMode redactionMode,
IReadOnlyDictionary<string, DataClassification> parametersToRedact);
/// <summary>
/// Formats a concrete path using pre-parsed segments.
/// </summary>
/// <param name="routeSegments">Parsed route segments.</param>
/// <param name="httpPath">Concrete request path.</param>
/// <param name="redactionMode">Redaction strategy.</param>
/// <param name="parametersToRedact">Classification map keyed by parameter name.</param>
/// <returns>Canonical formatted path.</returns>
public string Format(
in ParsedRouteSegments routeSegments,
string httpPath,
HttpRouteParameterRedactionMode redactionMode,
IReadOnlyDictionary<string, DataClassification> parametersToRedact);
/// <summary>
/// Formats the path and also returns the extracted parameter set (allocated convenience API).
/// </summary>
/// <param name="httpRoute">Route template.</param>
/// <param name="httpPath">Concrete request path.</param>
/// <param name="redactionMode">Redaction strategy.</param>
/// <param name="parametersToRedact">Classification map keyed by parameter name.</param>
/// <returns>A tuple of the formatted path and the extracted parameters.</returns>
public (string FormattedPath, IReadOnlyList<HttpRouteParameter> Parameters) FormatWithParameters(
string httpRoute,
string httpPath,
HttpRouteParameterRedactionMode redactionMode,
IReadOnlyDictionary<string, DataClassification> parametersToRedact);
}
}
### API Usage
Parse and reuse segments, extract parameters without allocation beyond initial array.
```csharp
var redactorProvider = GetRedactorProvider(); // Your IRedactorProvider
var parser = new HttpRouteParser(redactorProvider);
var segments = parser.ParseRoute("/api/items/{id}/{region?}");
var paramBuffer = new HttpRouteParameter[segments.ParameterCount];
if (parser.TryExtractParameters(
httpPath: "/api/items/42",
routeSegments: segments,
redactionMode: HttpRouteParameterRedactionMode.Strict,
parametersToRedact: new Dictionary<string, DataClassification> { { "id", MyTaxonomy.PrivateId } },
ref paramBuffer))
{
// paramBuffer[0].Name == "id"
// paramBuffer[0].Value == redacted value (e.g. "Redacted:42")
// Optional 'region' omitted: paramBuffer[1].Value == "" (default) or provided default if template had one.
}
Reuse parsed segments across multiple requests for performance.
var customerRoute = parser.ParseRoute("/customers/{customerId}/profile");
HttpRouteParameter[] buffer = new HttpRouteParameter[customerRoute.ParameterCount];
void HandleRequest(string path)
{
parser.TryExtractParameters(
path,
customerRoute,
HttpRouteParameterRedactionMode.Loose,
new Dictionary<string, DataClassification> { { "customerId", MyTaxonomy.PrivateId } },
ref buffer);
// buffer[0] now holds current customerId (possibly redacted).
}
Catch-all parameter example
var segmentsCatchAll = parser.ParseRoute("/files/{*path}");
string formattedFiles = formatter.Format(
"/files/{*path}",
"/files/a/b/c/report.txt",
HttpRouteParameterRedactionMode.Loose,
new Dictionary<string, DataClassification> { { "path", MyTaxonomy.PrivateId } });
// Catch-all redacted in Loose (classification supplied).
Format a path directly from template (canonical + redaction).
var formatter = new HttpRouteFormatter(parser, redactorProvider);
string formattedStrict = formatter.Format(
httpRoute: "/api/items/{id}/{region?}",
httpPath: "/api/items/42/eu",
redactionMode: HttpRouteParameterRedactionMode.Strict,
parametersToRedact: new Dictionary<string, DataClassification> { { "id", MyTaxonomy.PrivateId } });
// Example result: "api/items/Redacted:42/eu"
string formattedLoose = formatter.Format(
httpRoute: "/api/items/{id}/{region?}",
httpPath: "/api/items/42/eu",
redactionMode: HttpRouteParameterRedactionMode.Loose,
parametersToRedact: new Dictionary<string, DataClassification> { { "id", MyTaxonomy.PrivateId } });
// Example result: "api/items/Redacted:42/eu"
Formatting when optional parameter omitted: trailing slash normalized.
string formattedOmitted = formatter.Format(
httpRoute: "/api/items/{id}/{region?}",
httpPath: "/api/items/42",
redactionMode: HttpRouteParameterRedactionMode.Loose,
parametersToRedact: new Dictionary<string, DataClassification> { { "id", MyTaxonomy.PrivateId } });
// "api/items/Redacted:42"
Format plus get parameter values for structured telemetry.
var (path, paramList) = formatter.FormatWithParameters(
httpRoute: "/api/items/{id}/{region?}",
httpPath: "/api/items/42/eu",
redactionMode: HttpRouteParameterRedactionMode.Strict,
parametersToRedact: new Dictionary<string, DataClassification>
{
{ "id", MyTaxonomy.PrivateId },
{ "region", DataClassification.None } // explicit non-sensitive
});
// path: "api/items/Redacted:42/eu"
// paramList[0]: Name="id", IsRedacted=true
// paramList[1]: Name="region", IsRedacted=false
Metrics key (avoid leaking sensitive ids in Strict mode).
string metricKey = formatter.Format(
httpRoute: "/orders/{orderId}/lines/{lineId}",
httpPath: "/orders/123/lines/9",
redactionMode: HttpRouteParameterRedactionMode.Strict,
parametersToRedact: new Dictionary<string, DataClassification>());
// Both parameters redacted (no classifications + not well-known) -> "orders/[REDACTED]/lines/[REDACTED]" (placeholder depends on TelemetryConstants.Redacted).
Alternative Designs
No response
Risks
No response