Skip to content

Response Headers and ProblemDetails

RzR edited this page Jul 4, 2026 · 2 revisions

Response Headers and ProblemDetails

The Web integration shapes the HTTP response in two complementary ways:

  • Headers — emitted by PagedResponseHeaderWriter, invoked from PagedResultActionFilter (MVC) or PagedResultEndpointFilter (minimal API).
  • Body — JSON envelope on success (PagedResult<T>), RFC 9457 ProblemDetails on failure (built by the upstream IProblemDetailsResultFactory).

Headers

Header Source Default Notes
X-Total-Count IPagedResult.RowCount enabled Toggle with EmitTotalCountHeader.
X-Page-Count IPagedResult.PageCount enabled Toggle with EmitTotalCountHeader.
X-Page-Size IPagedResult.PageSize enabled Toggle with EmitTotalCountHeader.
X-Current-Page IPagedResult.CurrentPage enabled Toggle with EmitTotalCountHeader.
Link derived enabled RFC 5988. Only emitted when the request method is GET and PageCount > 0. The other query parameters are preserved verbatim and only page= is rewritten for first, prev, next, last.
Server-Timing IPagedResult.ExecutionDetails.ExecutionTimeMs disabled paged;dur=<ms>. Toggle with EmitServerTimingHeader. Skipped when ExecutionTimeMs == -1 (not measured).

The four X-* headers are toggled together by EmitTotalCountHeader because they constitute a single conceptual unit (the paging cursor).

The Link header is intentionally limited to GET because for POST/PUT the URL is not a fully meaningful representation of the request. If you serve paginated POST endpoints and want a navigable link header, consider exposing a parallel GET endpoint and setting Link from your own code.

Example

Request:

GET /api/products?page=2&pageSize=20&order=price:desc HTTP/1.1

Response:

HTTP/1.1 200 OK
Content-Type: application/json
X-Total-Count: 137
X-Page-Count: 7
X-Page-Size: 20
X-Current-Page: 2
Link: </api/products?pageSize=20&order=price:desc&page=1>; rel="first",
      </api/products?pageSize=20&order=price:desc&page=1>; rel="prev",
      </api/products?pageSize=20&order=price:desc&page=3>; rel="next",
      </api/products?pageSize=20&order=price:desc&page=7>; rel="last"
Server-Timing: paged;dur=42

How the writer locates the result

PagedResponseHeaderWriter.TryApply accepts any object and recognizes a paged result through three mechanisms, in order:

  1. The object directly implements IPagedResult (the non-generic abstraction).
  2. The object exposes a Value property (e.g. ObjectResult, JsonResult, Ok(...) minimal-API result). The writer unwraps the value via PagedAccessors (cached reflection delegates) and re-checks against IPagedResult.
  3. Implements IPagedResult<> for some T (recognized through reflection).

If none of the above matches, the writer is a no-op. This is why your action can return either PagedOkResult(paged) or a custom IActionResult that wraps the same envelope, and the headers will still appear.

Success body

The success body is the full PagedResult<T> envelope, serialized as JSON with the framework defaults (or the JsonSerializerOptions you set on PagedListResultWebOptions):

{
  "currentPage": 2,
  "pageCount": 7,
  "pageSize": 20,
  "rowCount": 137,
  "executionDetails": { "executionTimeMs": 42, "executionDate": "2025-01-01T12:00:00.000Z" },
  "response": [ { "id": 1, "name": "...", "price": 9.99 }, ... ],
  "isSuccess": true,
  "messages": []
}

For minimal-API endpoints, you must return paged.ToPagedHttpResult(ctx) (or Results.Ok(paged)) to keep the envelope. See Minimal API Integration.

Failure body (RFC 9457 ProblemDetails)

When the result envelope reports failure (IsSuccess == false) or the request fails validation, the response body is application/problem+json produced by the upstream RzR.ResultMessage.Web.IProblemDetailsResultFactory:

{
  "type": "https://datatracker.ietf.org/doc/html/rfc9457",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "detail": "Operator 'Between' requires a CompareValue (upper bound).",
  "instance": "/api/products",
  "errors": {
    "filter[1]": [ "Operator 'Between' requires a CompareValue (upper bound)." ]
  },
  "messages": [
    { "code": "...", "message": "..." }
  ]
}

The factory honors the following options:

  • PagedListResultWebOptions.ProblemTypeBaseUri — overrides the type URI.
  • PagedListResultWebOptions.StatusCodeMapper — lets you compute a custom status code from the failing IResult.

You can replace the entire factory by registering your own implementation of IProblemDetailsResultFactory before AddPagedListResultWeb:

services.AddSingleton<IProblemDetailsResultFactory, MyProblemFactory>();
services.AddPagedListResultWeb();

204 No Content

The minimal-API helper ToPagedHttpResult returns Results.NoContent() when the paged reference is null. This is intentional: a null paged result means the action decided there is no body to return (e.g. authorization stripped the response). Returning a 204 keeps the contract clean and avoids serializing a null envelope.

The MVC BaseApiPagedResultController does not have a built-in null shortcut; if you want the same behavior, write if (result is null) return NoContent(); at the top of your action.

Clone this wiki locally