Skip to content

Route-Level Final Response Hooks #174

Description

@eviltester

Summary

Add read-only route-level afterResponse hooks that run after Thingifier has produced the final HTTP response, including final status, headers, body where available, response policies, response views, and content negotiation.

Proposed API

thingifier.apiContract()
    .route(GET, "/todos")
    .afterResponse((ctx, response) -> {
        if (response.statusCode() == 200
                && "application/xml".equals(response.contentType())
                && ctx.requestHeaders().get("Accept").contains("application/xml")) {
            challenges.pass(ctx.authenticatedPrincipal(), CHALLENGE.GET_ACCEPT_XML);
        }
    });

Add to ThingifierApiRouteRule:

afterResponse(ThingifierApiResponseCallback callback)

Response view:

public final class ThingifierApiFinalResponse {
    int statusCode();

    /**
     * Final HTTP Content-Type header after content negotiation.
     * This is not the internal ApiResponse preferred/structured type.
     */
    String contentType();

    HttpHeadersBlock headers();

    /**
     * Final response body when available.
     * May be empty or unavailable for HEAD, streaming, or no-body responses.
     */
    Optional<String> body();

    ApiResponse apiResponse();
}

Context

The callback should receive a read-only route context with:

  • original public route path, e.g. /secret/note
  • matched route rule pattern
  • resolved Thingifier route info
  • resolved target entity and identifier, including fixed identifiers
  • relationship target details where applicable
  • request headers and query parameters
  • active data scope name and store
  • authenticated principals when present

For matched-route auth failures, afterResponse should still run for 401 and 403 responses. Principal accessors may return empty/null when authentication failed before a principal was established.

Runtime Order

  1. Match route
  2. Resolve scoped session
  3. Run explicit route authentication
  4. Run request validators
  5. Run entity/domain validation
  6. Execute operation
  7. Run existing operation callbacks/lifecycle hooks
  8. Apply route response policies, response views, and content negotiation
  9. Render final HttpApiResponse
  10. Run route-level afterResponse callbacks
  11. Run legacy/global HTTP response hooks

Behaviour

  • afterResponse is read-only in v1.
  • It must not mutate the already-produced response.
  • If a callback throws, Thingifier logs the exception and preserves the already-produced response.
  • Response mutation remains the job of route response policies or legacy HTTP response hooks.
  • Hooks run for generated routes, fixed routes, and custom route mappings.
  • Hooks run for successful responses and matched-route failures, including 401 and 403.
  • Hooks do not run for completely unmatched 404 responses in v1.
  • Callbacks are code-only and are not serialized to YAML/model export.
  • No OpenAPI changes are required.

Acceptance Criteria

  • A route can register an afterResponse callback.
  • The callback receives the final HTTP Content-Type after negotiation.
  • The callback receives the final status code.
  • The callback receives final headers.
  • The callback can inspect the body when available.
  • The callback receives public route path and resolved target route details.
  • Fixed routes expose the resolved fixed identifier.
  • Matched-route 401/403 responses invoke the callback with no authenticated principal when auth failed.
  • Callback exceptions are logged and do not change the response.
  • Existing lifecycle hooks, operation callbacks, response policies, and legacy HTTP response hooks remain backwards compatible.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions