Skip to content

Match outgoing requests

Run the complete page example.

A test needs to know whether your app asked for the right thing. Checking only the returned value can miss a wrong URL, a missing header or an incorrect request body.

Refit's route matchers let you describe the request you expect and choose its reply. Start with a method and path, then add conditions for the details that matter to your test.

Choose a method and path

1. Add expectations. Route.Get, Post, Put, Delete, Patch and Head select their HTTP methods. Route.Any accepts any method. Route.For accepts an HttpMethod, such as OPTIONS. Every factory accepts a path template. Route.Fallback() supplies a reusable catch-all.

2. Send through HttpClient. A raw client is useful for checking handler rules alone. For a full Refit test, use the generated client and JSON context. Raw HTTP does not need JSON metadata unless you serialize or deserialize models.

3. Check the status. A fallback declared first cannot hide a one-shot expectation.

using StubHttp http = new()
{
    {
        Route.Fallback(),
        Reply.Status(HttpStatusCode.NotFound)
    },
    {
        Route.Get("/get"),
        Reply.Status(HttpStatusCode.OK)
    },
    {
        Route.Post("/post"),
        Reply.Status(HttpStatusCode.Created)
    },
    {
        Route.Put("/put"),
        Reply.Status(HttpStatusCode.NoContent)
    },
    {
        Route.Delete("/delete"),
        Reply.Status(HttpStatusCode.NoContent)
    },
    {
        Route.Patch("/patch"),
        Reply.Status(HttpStatusCode.NoContent)
    },
    {
        Route.Head("/head"),
        Reply.Status(HttpStatusCode.OK)
    },
    {
        Route.For(HttpMethod.Options, "/options"),
        Reply.Status(HttpStatusCode.OK)
    },
    {
        Route.Any("/any"),
        Reply.Status(HttpStatusCode.Accepted)
    },
};
using HttpClient client = CreateClient(http);
using HttpResponseMessage get = await client.GetAsync(new Uri("https://people.example/get"));
SampleCheck.Equal(HttpStatusCode.OK, get.StatusCode);
using HttpResponseMessage missing = await client.GetAsync(new Uri("https://people.example/missing"));
SampleCheck.Equal(HttpStatusCode.NotFound, missing.StatusCode);

Path and priority rules

Template is required. A leading / matches the request's absolute path. An absolute template compares scheme, host and path. "*" accepts any path. A complete {name} segment matches one nonempty segment. It does not expose a captured parameter. An embedded placeholder, such as person-{id}, is literal text. Literal segments compare case-sensitively. Segment counts and trailing slashes matter. A query written inside a template is ignored. Set a query property to check it.

The handler tries one-shot expectations, then reusable routes, then fallbacks. Within each group it uses declaration order. It does not rank routes by path detail. A one-shot route is consumed after matching. Add identical routes in order to supply a sequence of replies. Reusable=true allows repeated matches and removes the verification requirement. Fallback=true gives the final priority and also removes the verification requirement. A manually configured fallback must pass its other conditions. Route.Fallback() accepts everything. An unmatched request throws InvalidOperationException; it does not return an automatic 404.

Check queries, headers and bodies

The constraint example sets every matching property and sends a form body that satisfies them.

PropertyRequired request behavior
MethodSame HTTP method. Null accepts any method.
QueryContains each decoded key/value pair. Extra pairs are allowed.
ExactQuerySame decoded pairs and count as the supplied encoded query, ignoring order. Omit the leading ?.
ExactQueryParamsSame decoded pairs and count as the supplied array, ignoring order.
HeadersContains each name/value pair in request or content headers. Names use HTTP header lookup. Values compare exactly. Multiple values join with ", ".
BodyExact body text. Missing content counts as an empty string.
FormDataContains each decoded form pair. Extra pairs are allowed. The media type is not checked.
WhereThe synchronous predicate returns true.
WhereAsyncThe asynchronous predicate returns true. It runs after Where passes.

Query and form decoding converts + to a space and decodes percent escapes. Bare keys have empty values. Empty entries between & characters are ignored. Keys and values compare case-sensitively. All three query properties can be used together. ExactQuery checks decoded content, despite its string argument. It does not check raw byte spelling or pair order. Predicates run after the built-in checks and receive no cancellation token. Body buffering makes content rereadable when buffering succeeds.

Duplicate-query discrepancy

The implementation compares pair count and membership. It does not check how many times a pair occurs. Expected behavior: two expected a=1 pairs require two actual a=1 pairs. Actual behavior: a=1&b=2 passes because the count is two and a=1 is present. The same discrepancy affects both exact-query properties. The runnable example asserts the actual response, rather than hiding the defect:

using StubHttp http = new()
{
    {
        new RouteMatcher { Template = "/query", ExactQueryParams = [("a", "1"), ("a", "1")] },
        Reply.Text(AcceptedReply)
    },
    {
        new RouteMatcher { Template = "/query", ExactQuery = "a=1&a=1" },
        Reply.Text(AcceptedReply)
    },
};
using HttpClient client = CreateClient(http);
using HttpResponseMessage pairs = await client.GetAsync(new Uri("https://people.example/query?a=1&b=2"));
using HttpResponseMessage encoded = await client.GetAsync(new Uri("https://people.example/query?a=1&b=2"));
SampleCheck.Equal(AcceptedReply, await pairs.Content.ReadAsStringAsync());
SampleCheck.Equal(AcceptedReply, await encoded.Content.ReadAsStringAsync());

Avoid duplicate exact-query expectations. If duplicates matter, parse and count pairs in Where. Also send one-shot tests serially. Matching and consumption are separate steps. Concurrent requests can both select the same one-shot route before either consumes it. That is a race: the result depends on how requests overlap.

Complete constraint and enumeration excerpts

The complete constraint test uses raw form data. No serializer metadata is needed for this body.

RouteMatcher route = new()
{
    Method = HttpMethod.Post,
    Template = "/people/{id}",
    Query = [("mode", "short")],
    ExactQuery = "extra=1&mode=short",
    ExactQueryParams = [("mode", "short"), ("extra", "1")],
    Headers = [("X-Test", "yes"), ("Content-Type", "application/x-www-form-urlencoded")],
    Body = "name=Ada+Lovelace&extra=1",
    FormData = [("name", "Ada Lovelace")],
    Where = static request => request.RequestUri!.Host == "people.example",
    WhereAsync = static async request => (await request.Content!.ReadAsStringAsync()).Contains(PersonName, StringComparison.Ordinal),
    Reusable = true,
    Fallback = false,
};
using StubHttp http = new() { { route, Reply.Text("matched") } };
using HttpClient client = CreateClient(http);
using HttpRequestMessage request = new(HttpMethod.Post, "https://people.example/people/7?mode=short&extra=1")
{
    Content = new FormUrlEncodedContent([new("name", "Ada Lovelace"), new("extra", "1")]),
};
request.Headers.Add("X-Test", "yes");
using HttpResponseMessage response = await client.SendAsync(request);
SampleCheck.Equal("matched", await response.Content.ReadAsStringAsync());
Verify(http);

Both enumeration interfaces return snapshots of the configured routes. The example checks each route and confirms the nine-entry table size.

int genericCount = 0;
foreach (RouteMatcher route in (IEnumerable<RouteMatcher>)http)
{
    SampleCheck.Equal(true, route.Template.Length > 0);
    genericCount++;
}

SampleCheck.Equal(RouteCount, genericCount);
int count = 0;
foreach (object route in (IEnumerable)http)
{
    SampleCheck.Equal(true, route is RouteMatcher);
    count++;
}

SampleCheck.Equal(RouteCount, count);

API reference

APIDescriptionParameters or valueReturns and behavior
RouteProvides static factories for common request matchers.Static class; do not create an instance.Each factory returns a configured RouteMatcher.
Route.Any(string template)Matches a path regardless of its HTTP method.template: a relative or absolute path template; a complete {name} segment matches one path segment.Returns a RouteMatcher with no method restriction.
Route.Get(string template)Matches a GET request for a path.template: the relative or absolute path template to match.Returns a matcher whose method is GET.
Route.Post(string template)Matches a POST request for a path.template: the relative or absolute path template to match.Returns a matcher whose method is POST.
Route.Put(string template)Matches a PUT request for a path.template: the relative or absolute path template to match.Returns a matcher whose method is PUT.
Route.Delete(string template)Matches a DELETE request for a path.template: the relative or absolute path template to match.Returns a matcher whose method is DELETE.
Route.Patch(string template)Matches a PATCH request for a path.template: the relative or absolute path template to match.Returns a matcher whose method is PATCH.
Route.Head(string template)Matches a HEAD request for a path.template: the relative or absolute path template to match.Returns a matcher whose method is HEAD.
Route.For(HttpMethod method, string template)Matches a path for an HTTP method that has no convenience factory, such as OPTIONS.method: the HttpMethod to require; template: the relative or absolute path template to match.Returns a matcher for the supplied method and template.
Route.Fallback()Creates a catch-all route tried after every one-shot and reusable route.None.Returns a matcher with Template set to "*" and Fallback set to true; it may match repeatedly.
RouteMatcherDescribes the request that a route table entry accepts.Set Template and any init-only conditions in an object initializer.A configured matcher is paired with a Reply in StubHttp.
RouteMatcher()Creates a matcher for custom conditions.None.Returns a matcher with optional conditions unset. Set its required Template before it is added to a route table.
RouteMatcher.MethodRestricts a matcher to one HTTP method.Init-only HttpMethod?; null is the default.A non-null value must equal the request method. null accepts every method.
RouteMatcher.TemplateSupplies the path pattern every matcher needs.Required init-only string: a relative or absolute path, or "*" for every path.The handler matches this template against the request URI.
RouteMatcher.QueryRequires selected decoded query pairs.Init-only nullable array of (string Key, string Value) pairs to find.Every supplied pair must occur; the request may contain other pairs.
RouteMatcher.ExactQueryRequires the complete decoded query from encoded text.Init-only nullable string without a leading ?.Requires the same decoded pair count and members, ignoring order.
RouteMatcher.ExactQueryParamsRequires the complete decoded query from named pairs.Init-only nullable array of (string Key, string Value) pairs.Requires the same pair count and members, ignoring order.
RouteMatcher.HeadersRequires selected request or content headers.Init-only nullable array of (string Name, string Value) pairs.Every supplied header name and value must occur.
RouteMatcher.BodyRequires an exact text request body.Init-only nullable string containing the expected body.The request body must equal the value. Missing content is an empty string.
RouteMatcher.FormDataRequires selected decoded form fields.Init-only nullable array of (string Key, string Value) pairs to find in the body.Every supplied form pair must occur; extra pairs and the media type are ignored.
RouteMatcher.WhereAdds a synchronous check for details the built-in properties do not cover.Init-only nullable Func<HttpRequestMessage, bool>; its HttpRequestMessage argument is the request being matched.The route matches only when the predicate returns true.
RouteMatcher.WhereAsyncAdds an asynchronous check, such as one that reads the request body.Init-only nullable Func<HttpRequestMessage, Task<bool>>; use Task to return the result.The route matches only when the task completes with true, after Where passes.
RouteMatcher.ReusableMakes a route available for repeated background behavior.Init-only bool; default false.true allows repeated matches and excludes the route from VerifyAllCalled.
RouteMatcher.FallbackMakes a route the final match attempt.Init-only bool; default false.true gives the route fallback priority, allows repeated matches, and excludes it from VerifyAllCalled.
StubHttp.GetEnumerator() (explicit IEnumerable<RouteMatcher>)Lets you enumerate configured matchers as RouteMatcher values.None; cast StubHttp to IEnumerable<RouteMatcher> to call it.Returns an IEnumerator<RouteMatcher> over a snapshot of the route table.
StubHttp.GetEnumerator() (explicit IEnumerable)Lets non-generic code enumerate the configured matchers.None; cast StubHttp to IEnumerable to call it.Returns a non-generic IEnumerator over the same route snapshot.