# 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