Building requests
Proto.Context.Rest(name) returns a RestRequestBuilder. Configure it fluently, then finish with a verb method.
using var response = await Proto.Context.Rest()
.Header("X-Correlation-Id", correlationId)
.Body(new CreateOrderRequest("observability-seat", 12, 19.95m))
.PostAsync("/api/orders");
Verbs
| Verb | Sends |
|---|---|
GetAsync(route, params?, ct?) | GET |
PostAsync(route, params?, ct?) | POST |
PutAsync(route, params?, ct?) | PUT |
PatchAsync(route, params?, ct?) | PATCH |
DeleteAsync(route, params?, ct?) | DELETE |
HeadAsync(route, params?, ct?) | HEAD |
OptionsAsync(route, params?, ct?) | OPTIONS |
SendAsync(method, route, params?, ct?) | any HttpMethod, for verbs with no helper |
Each returns Task<RestResponse> and takes the same route template and optional route-and-query object.
RestResponse is IDisposable; use using var (the response body is already buffered, so disposing doesn't cut anything short).
Route templates and parameters
The second argument fills {placeholders} in the route; whatever is left over becomes the query string.
await Proto.Context.Rest().GetAsync(
"/api/workspaces/{workspaceId}/releases",
new { workspaceId = workspace.Id, state = "deployed", page = 2 });
// → /api/workspaces/42/releases?state=deployed&page=2
The rules:
| Input | Becomes |
|---|---|
{placeholder} with a matching value | a path segment, matched case-insensitively ({workspaceId}=42 → /workspaces/42) |
{placeholder} with no value, or a null one | ArgumentException naming the parameter |
| leftover non-null property | a query parameter (state="deployed" → ?state=deployed) |
| null property, or an empty collection | dropped (nothing emitted) |
| collection value | a repeated key (tags=[a,b] → tags=a&tags=b) |
bool, date or time value | invariant text (true, round-trip "O" dates) |
#fragment in the template | preserved, re-appended after the query string |
| absolute URL as the template | used as-is; the client's base address is bypassed |
| non-HTTP(S) scheme, or a relative route with no base address | InvalidOperationException |
colon in the first segment (orders:search) | a relative path, as RFC 3986 requires |
Keys and values are escaped with Uri.EscapeDataString. A dictionary with string keys works in place of the object.
A per-test base-address resolver wins over the client's HttpClient.BaseAddress at request time.
Bodies
RestRequestBuilder Body(object payload, JsonSerializerOptions? options = null); // JSON
RestRequestBuilder Body(string rawContent, string mediaType = "text/plain");
RestRequestBuilder Body(ReadOnlyMemory<byte> content, string mediaType = "application/octet-stream");
RestRequestBuilder Body(Func<HttpContent> contentFactory);
The object overload serialises as application/json. The factory overload is invoked per send, so a re-sent request gets fresh content.
Headers
RestRequestBuilder Header(string name, string value);
Header names are case-insensitive and the last value for a name wins. ProtoTest tries the request headers first and falls back to the content headers, throwing InvalidOperationException if neither accepts it. The trace records header names and the count, not header values.
Authentication
RestRequestBuilder Auth(IProtoHttpAuthenticator authenticator);
RestRequestBuilder Auth<TAuthenticator>(params object[] constructorArgs);
RestRequestBuilder WithoutAuth();
See Authentication for how these interact with [Auth<T>].
Response size limit
Responses are buffered with a cap of 10 MiB by default. A known Content-Length above the cap fails before the body is read; otherwise the buffer stops mid-read. Either way the failure is ProtoResponseTooLargeException, whose MaximumBytes and ObservedBytes tell you the configured cap and the observed size. HEAD, 204, 304 and 1xx responses never carry a body, so their headers are not measured against the cap. Change it in code or configuration:
builder.AddApplication("Api", app => app.AddRest(rest => rest
.ConfigureResponses(options => options.MaxResponseBodyBytes = 32 * 1024 * 1024)
.AddClient("Api")));
{ "ProtoTest": { "Rest": { "Responses": { "MaxResponseBodyBytes": 33554432 } } } }
MaxDiagnosticBodyLength (64 KiB) bounds the response body embedded in a status-assertion failure message and in captured attachments. See Attachments for the capture options.