# Response The **Response** is an optional component of every {doc}`/event`. When an event declares a `response` field, the response carries the result of processing back to the event's origin. --- ## How responses work ```python @response class MyResponse(Response): result: str count: int @event class MyEvent(Event): response: MyResponse ``` Key mechanics: 1. The `@event` decorator detects `response: MyResponse` and: * Validates that `MyResponse` is a subclass of `Response`. * Makes the `response` field `init=False`. * Auto-instantiates it via `default_factory`. 2. When a Manager handler modifies `event.response`, the response is **automatically sent** when the handler returns. 3. You never pass `response=...` when constructing an event. --- ## Response Status Every response has a **status** — an internal attribute that tracks processing state. ### Status values | Status | Meaning | Trigger | |------------|------------------------------------------|--------------------------------------------------------------| | `NONE` | Default — no processing has occurred | Initial state before any handler runs | | `OK` | Processing succeeded | Automatically set when any response field is assigned a value, or via explicit `set_status(ResponseStatus.OK)` | | `FAILED` | Processing failed | Set explicitly via `set_status(ResponseStatus.FAILED)` | ### Auto-status behavior ```python # In a Manager handler: event.response.result = "done" # ← status auto-set to OK ``` ```python # Explicit status: event.response.set_status(ResponseStatus.FAILED) event.response.set_status(ResponseStatus.OK) ``` ### Status vs HTTP status **Important:** `ResponseStatus.OK` / `ResponseStatus.FAILED` do **not** control HTTP status codes. They are internal EDRI status indicators. HTTP status is controlled by **response directives** (see below). By default, the HTTP status is **200**. Directives can override it. | Internal Status | Default HTTP Status | When overridden | |-----------------|---------------------|-------------------------------| | `OK` | 200 | Via directives (e.g., redirect → 302) | | `FAILED` | 200 | Via directives (e.g., NotFoundResponseDirective → 404) | --- ## Directives Directives are instructions that modify how the API layer translates a response for the client — across any transport it supports (HTTP, WebSocket, and any API endpoints added in the future), not HTTP alone. They are attached to `event.response` using `add_directive(directive, /, *, ignore_status=False)`. Adding a directive normally also marks the response as changed and, if it had no explicit status yet, sets its status to `OK`. Pass `ignore_status=True` to append the directive **without** that side effect — used when decorating a response that must keep its existing status (e.g. a `FAILED` error response). ### Directive types (base — usable for both HTTP and WebSocket) | Directive Class | HTTP Effect | Typical Use Case | |----------------------------------------|-----------------------------------|-------------------------------------| | `InternalServerErrorResponseDirective` | 500 Internal Server Error | Unexpected errors | | `UnauthorizedResponseDirective` | 401 Unauthorized | Authentication failures | These live in `edri.dataclass.directive.base` and extend `ResponseDirective` directly, so they work on both HTTP and WebSocket transports. ### Directive types (HTTP) | Directive Class | HTTP Effect | Typical Use Case | |----------------------------------------|-----------------------------------|-------------------------------------| | `CookieResponseDirective` | `Set-Cookie` header | Session management, preferences | | `AccessDeniedResponseDirective` | 403 Forbidden | Authorization failures | | `NotFoundResponseDirective` | 404 Not Found | Resource not found | | `ConflictResponseDirective` | 409 Conflict | Resource state conflicts | | `BadRequestResponseDirective` | 400 Bad Request | Invalid client input | | `UnprocessableContentResponseDirective`| 422 Unprocessable Content | Semantically invalid request | | `UnsupportedMediaTypeResponseDirective`| 415 Unsupported Media Type | Request payload in an unaccepted format | | `NotModifiedResponseDirective` | 304 Not Modified | Conditional GET (ETag match) | | `ServiceUnavailableResponseDirective` | 503 Service Unavailable | Temporarily overloaded/down | | `NotImplementedResponseDirective` | 501 Not Implemented | Requested functionality unsupported | | `PartialContentResponseDirective` | 206 Partial Content | Successful byte-range response | | `RangeNotSatisfiableResponseDirective`| 416 Range Not Satisfiable | Invalid or unsatisfiable Range | | `CreatedResponseDirective` | 201 Created (+ optional `Location`) | Resource created | | `AcceptedResponseDirective` | 202 Accepted | Accepted for async processing | | `NoContentResponseDirective` | 204 No Content | Success with no response body | | `MovedPermanentlyResponseDirective` | 301 Moved Permanently (+ `Location`) | Resource permanently moved | | `FoundResponseDirective` | 302 Found (+ `Location`) | Temporary redirect (common default) | | `SeeOtherResponseDirective` | 303 See Other (+ `Location`) | Post/Redirect/Get | | `TemporaryRedirectResponseDirective` | 307 Temporary Redirect (+ `Location`) | Temporary redirect, keeps method | | `PermanentRedirectResponseDirective` | 308 Permanent Redirect (+ `Location`) | Permanent redirect, keeps method | | `GoneResponseDirective` | 410 Gone | Resource permanently removed | | `TooManyRequestsResponseDirective` | 429 Too Many Requests (+ optional `Retry-After`) | Rate limiting | | `HeaderResponseDirective` | No status change (only adds header) | Setting arbitrary response headers | All HTTP directives live in `edri.dataclass.directive.http`. ### Directive types (HTML) | Directive Class | Effect | Typical Use Case | |----------------------------------|------------------------------------------|----------------------------------------------| | `RedirectResponseDirective` | Browser redirect (302) | Form submission → thank-you page | `RedirectResponseDirective` lives in `edri.dataclass.directive.html` and is meant for **HTML endpoints** — it can take an API Event class and resolve it to the target URL (see below). For **REST/HTTP** endpoints, use the per-status redirect directives from `edri.dataclass.directive.http` instead — `MovedPermanentlyResponseDirective` (301), `FoundResponseDirective` (302), `SeeOtherResponseDirective` (303), `TemporaryRedirectResponseDirective` (307), and `PermanentRedirectResponseDirective` (308). Each takes a `location` string and emits the matching 3xx status together with a `Location` header. --- ## Using directives ### Adding a directive ```python from edri.dataclass.directive.http import NotFoundResponseDirective event.response.add_directive(NotFoundResponseDirective()) ``` ### Adding a redirect (to a URL) ```python from edri.dataclass.directive.html import RedirectResponseDirective event.response.set_status(ResponseStatus.OK) event.response.add_directive(RedirectResponseDirective("/path/to/page")) ``` ### Adding a redirect (to an API Event) ```python event.response.add_directive( RedirectResponseDirective(ContactThanks) ) ``` The framework resolves `ContactThanks` to the URL declared in its `@api(url=...)` decorator. For events with path placeholders: ```python event.response.add_directive( RedirectResponseDirective(UserPage, user_id=42) ) ``` ### Setting a cookie ```python from edri.dataclass.directive.http import CookieResponseDirective event.response.add_directive( CookieResponseDirective( name="session_id", value="abc123", httponly=True, secure=True, path="/", max_age=3600, ) ) ``` Available `CookieResponseDirective` attributes: | Attribute | Type | Description | |------------|--------------------|------------------------------------------| | `name` | `str` | Cookie name (required) | | `value` | `str` | Cookie value (required) | | `expires` | `datetime \| None` | Expiration date, rendered as RFC 1123/GMT | | `path` | `str \| None` | Cookie path | | `domain` | `str \| None` | Cookie domain | | `max_age` | `int \| None` | Max age in seconds | | `secure` | `bool` | HTTPS only | | `httponly` | `bool` | Not accessible via JavaScript | | `samesite` | `str \| None` | SameSite policy (`Lax`, `Strict`, `None`) | | `comment` | `str \| None` | Cookie comment | | `version` | `int \| None` | Cookie version | > **Security note:** Default values for `secure`, `httponly`, and `samesite` are **not safe** for production. The framework defaults (`secure=False`, `httponly=False`, `samesite=None`) are permissive for development convenience. Explicitly set `secure=True`, `httponly=True`, and a `samesite` policy in production. ### Access denied ```python from edri.dataclass.directive.http import AccessDeniedResponseDirective event.response.set_status(ResponseStatus.FAILED) event.response.add_directive( AccessDeniedResponseDirective(message="Admin access required") ) ``` ### Internal server error ```python from edri.dataclass.directive.base import InternalServerErrorResponseDirective event.response.set_status(ResponseStatus.FAILED) event.response.add_directive( InternalServerErrorResponseDirective(message="Database connection failed") ) ``` --- ## HTTP status code mapping When the API layer processes a response, it determines the HTTP status code as follows: 1. Start with **200** (default). 2. If any directive overrides the status, that status is used. 3. If multiple directives set the status, **last wins**. | Directive | HTTP Status | |------------------------------------------|--------------------------------| | None (default) | 200 OK | | `RedirectResponseDirective` | 302 Found (HTML only) | | `NotFoundResponseDirective` | 404 Not Found | | `AccessDeniedResponseDirective` | 403 Forbidden | | `UnauthorizedResponseDirective` | 401 Unauthorized | | `InternalServerErrorResponseDirective` | 500 Internal Server Error | | `BadRequestResponseDirective` | 400 Bad Request | | `ConflictResponseDirective` | 409 Conflict | | `UnprocessableContentResponseDirective` | 422 Unprocessable Content | | `NotModifiedResponseDirective` | 304 Not Modified | | `ServiceUnavailableResponseDirective` | 503 Service Unavailable | | `PartialContentResponseDirective` | 206 Partial Content | | `RangeNotSatisfiableResponseDirective` | 416 Range Not Satisfiable | | `HeaderResponseDirective` | No status change (only adds header) | | `CookieResponseDirective` | No status change (only adds cookie) | --- ## Response lifecycle ```mermaid flowchart TD Recv["Event received by Manager"] --> Auto["Event.response
auto-instantiated
status = NONE"] Auto --> Exec["Handler executes"] Exec --> A1["field assignment → status auto-set to OK"] Exec --> A2["set_status(FAILED) → status = FAILED"] Exec --> A3["add_directive(...) → directive added"] A1 --> Ret["Handler returns"] A2 --> Ret A3 --> Ret Ret --> Mod{response modified?} Mod -->|yes| Send["sent automatically through Router"] Mod -->|no| Nothing["nothing sent (fire-and-forget)"] ``` --- ## Custom responses Responses can be extended with custom fields: ```python @response class PaginatedResponse(Response): items: list[dict] total_count: int page: int page_size: int ``` Responses can also use multiple inheritance: ```python @response class TimestampedResponse(Response): created_at: str updated_at: str @response class AuditResponse(TimestampedResponse): action: str user_id: int ``` --- ## Gotchas & Notes * Responses are **optional**. If an event does not declare `response`, no response is sent. * Setting `event.response` in a Manager handler automatically sends it when the handler returns. * In a **Worker**, you must call `self.event_send(event)` explicitly — auto-send does not apply. * `ResponseStatus.FAILED` does **not** produce an HTTP error status by itself. Use directives for HTTP status control. * Directive "last wins" applies per type — you can add a cookie directive and a redirect directive in the same response. * HTTP/HTML-specific directives are ignored on WebSocket responses. Base error directives such as `InternalServerErrorResponseDirective` and `UnauthorizedResponseDirective` are handled by the WebSocket layer and produce an error-shaped response. * Response fields whose name starts with `_` are **concealed**: stripped from JSON (REST/WebSocket) output but still passed to HTML templates. Use this for server-side-only data. See {ref}`concealed-fields-api-output`. --- ## See also * {doc}`/event` — defining events and response types * {doc}`/api_event` — how responses map to API output * {doc}`/api` — API layer, Accept header, serialization * {doc}`/manager` — how managers send responses automatically * {doc}`/worker` — how workers must send responses explicitly