The body does not replace status
A string returned directly by a Minimal API is written as text with text/plain under default behavior. An ordinary object is serialized as JSON. Neither choice should be confused with status selection: TypedResults.Ok("404") still represents HTTP success even though its text resembles an error code.
If the contract requires 404 for a missing resource, use a result expressing that status. An object { error = "missing" } inside Ok makes the client infer a contradictory convention. First define the operation outcome, then its corresponding status, fields and representation.
Typed handler alternatives
TypedResults returns concrete result types. Results<Ok<Item>, NotFound> declares two alternatives and retains metadata useful for describing the response. The handler can return Ok<Item> or NotFound; adding BadRequest in that body requires revising the return type to include the alternative.
This constrains function returns. A request can fail authentication, routing or binding before Find. Do not interpret the union as a guarantee that the whole application can only respond 200 or 404. The public contract must consider those boundaries and the middleware actually configured.
Description does not execute the operation
A concrete TypedResults type can supply response metadata automatically. If a helper declares only IResult, its signature loses some of that static information. Retaining the concrete type, declaring the right union or supplying consistent explicit metadata are ways to restore a useful description.
That metadata saves no data, checks no permissions and does not turn an Ok branch into an error. A description advertising 404 while the handler returns 200 with error=missing needs a behavior correction. Reviews should compare signature, branches and description without using documentation as proof of effects the code does not perform.
Compose Problem Details
AddProblemDetails registers the error representation service. Exception and status-code middleware connect pipeline situations to that service. In the example, UseExceptionHandler handles downstream exceptions and UseStatusCodePages can produce a representation for bodyless error statuses when writing is supported.
Registration alone does not automatically wrap every handler in a catch. Nor does it promise to rewrite a body already emitted. To accept the contract, identify error origin, body presence, response-start timing and the available writer. Only then conclude which public representation can be produced.
Public detail and diagnostics
The Developer Exception Page is intended for development. It can include stack traces, query parameters, cookies and headers. Exposing it to production visitors turns investigation detail into public data without the client contract requiring it.
A public error should let clients interpret failure through bounded fields. Investigation can use appropriate internal records with control over what is collected. Hiding the stack while copying SQL messages, cookies or the entire request into detail does not repair that boundary. The Problem Details format does not automatically make arbitrary text safe either.
Negotiate errors too
The default Problem Details writer supports JSON representations and documented compatible Accept types. A client accepting only application/xml does not gain XML support merely because AddProblemDetails exists. If TryWriteAsync returns false, the branch needs an explicit policy; do not document a body that was not written.
Options include agreeing supported formats with the client, supplying an appropriate writer or defining public fallback and negotiation. Repeating the same call does not add support. The choice must preserve failure status and remain consistent with the contract, including situations where the response can no longer be changed.
Read the complete contract
The example catalogue is deliberately fixed: only Item 42 exists. For /items/42, Find returns Ok<Item>; for /items/7, NotFound. With compatible Accept, status handling can fill the bodyless error. For /items/blue, int binding fails before Find. There is no database or claim of production lookup.
When reviewing response alternatives, distinguish handler outcome from final pipeline outcome. Evidence of an earlier 400 does not invalidate the function union; it shows another boundary to document. Questions ask you to interpret these branches and choose a contract correction without executing C# or publishing an API.
// Program.cs: ASP.NET Core 10.0 Web SDK, net10.0, ImplicitUsings enabled.
// Fixed teaching catalogue: item 42 exists; no database or external call.
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();
app.UseStatusCodePages();
app.MapGet("/items/{id}", Find);
app.Run();
static Results<Ok<Item>, NotFound> Find(int id)
{
if (id == 42)
return TypedResults.Ok(new Item(42, "Training item"));
return TypedResults.NotFound();
}
public sealed record Item(int Id, string Name);
With Accept compatible with the writer, /items/42 returns 200 with the model Item. /items/7 returns 404 and middleware can produce Problem Details for the bodyless error. /items/blue fails binding before Find. The handler union does not enumerate that 400.
Common pitfalls
Returning Ok with text 404; assuming TypedResults proves persistence; treating the return union as a list of all pipeline errors; promising error JSON without checking writer support and negotiation.
Related topics: Routing, binding and validation · Test scope and integration evidence · Health, observability and diagnosis
Status and representation must reflect the actual outcome. Types and metadata help describe the handler; pipeline composition and error policy still determine other responses.
Reference: Plain string response · ASP.NET Core 10.0 / .NET 10 / C# 14