Response

The Response is an optional component of every Event. When an event declares a response field, the response carries the result of processing back to the event's origin.


How responses work

@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

# In a Manager handler:
event.response.result = "done"  # ← status auto-set to OK
# 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

from edri.dataclass.directive.http import NotFoundResponseDirective

event.response.add_directive(NotFoundResponseDirective())

Adding a redirect (to a URL)

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)

event.response.add_directive(
    RedirectResponseDirective(ContactThanks)
)

The framework resolves ContactThanks to the URL declared in its @api(url=...) decorator. For events with path placeholders:

event.response.add_directive(
    RedirectResponseDirective(UserPage, user_id=42)
)

Access denied

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

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

        flowchart TD
    Recv["Event received by Manager"] --> Auto["Event.response<br/>auto-instantiated<br/>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:

@response
class PaginatedResponse(Response):
    items: list[dict]
    total_count: int
    page: int
    page_size: int

Responses can also use multiple inheritance:

@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 Concealed fields (API output).


See also

  • Event — defining events and response types

  • API Event — how responses map to API output

  • API — API layer, Accept header, serialization

  • Manager — how managers send responses automatically

  • Worker — how workers must send responses explicitly