# Directives A **directive** is a post-processing instruction attached to a {class}`~edri.dataclass.response.Response` that modifies how the API layer translates the response for the client — across HTTP, WebSocket, or any transport. Directives are the mechanism by which handlers control HTTP status codes, set headers and cookies, trigger redirects, and signal error conditions, all without coupling business logic to transport details. --- ## Hierarchy ``` ResponseDirective # edri.dataclass.directive ├── HTTPResponseDirective # General HTTP directives │ ├── HTMLResponseDirective # HTML-specific (redirect with event resolution) │ │ └── RedirectResponseDirective │ └── RESTResponseDirective # REST-specific ├── WebSocketResponseDirective # WebSocket-specific (reserved for future use) ├── InternalServerErrorResponseDirective # 500 — transport-agnostic └── UnauthorizedResponseDirective # 401 — transport-agnostic ``` All directive classes are frozen dataclasses and live in the `edri.dataclass.directive` package: | Module | Contents | |--------|----------| | `edri.dataclass.directive.__init__` | Base classes: `ResponseDirective`, `HTTPResponseDirective`, `HTMLResponseDirective`, `RESTResponseDirective`, `WebSocketResponseDirective` | | `edri.dataclass.directive.base` | `InternalServerErrorResponseDirective`, `UnauthorizedResponseDirective` | | `edri.dataclass.directive.http` | All HTTP status directives, `CookieResponseDirective`, `HeaderResponseDirective` | | `edri.dataclass.directive.html` | `RedirectResponseDirective` (event-aware HTML redirect) | --- ## Concrete class catalogue ### Transport-agnostic (HTTP + WebSocket) | Directive | HTTP status | When to use | |-----------|-------------|-------------| | `InternalServerErrorResponseDirective` | 500 | Unexpected server errors | | `UnauthorizedResponseDirective` | 401 | Missing or invalid authentication | These extend `ResponseDirective` directly and are handled by both the HTTP and WebSocket protocol layers. ### HTTP directives | Directive | HTTP status | When to use | |-----------|-------------|-------------| | `BadRequestResponseDirective` | 400 | Malformed or invalid client input | | `UnauthorizedResponseDirective` | 401 | Authentication failure | | `AccessDeniedResponseDirective` | 403 | Authorisation failure | | `NotFoundResponseDirective` | 404 | Resource not found | | `ConflictResponseDirective` | 409 | Resource state conflict | | `UnprocessableContentResponseDirective` | 422 | Semantically invalid request body | | `UnsupportedMediaTypeResponseDirective` | 415 | Request payload in an unaccepted format | | `TooManyRequestsResponseDirective` | 429 | Rate limiting (optional `retry_after`) | | `InternalServerErrorResponseDirective` | 500 | Unexpected errors | | `NotImplementedResponseDirective` | 501 | Functionality not supported | | `ServiceUnavailableResponseDirective` | 503 | Temporary overload or maintenance | | `NotModifiedResponseDirective` | 304 | Conditional GET — ETag match, no body | | `PartialContentResponseDirective` | 206 | Successful byte-range response | | `RangeNotSatisfiableResponseDirective` | 416 | Invalid or unsatisfiable `Range` | | `GoneResponseDirective` | 410 | Resource permanently removed | | `CreatedResponseDirective` | 201 | Resource created (optional `Location`) | | `AcceptedResponseDirective` | 202 | Accepted for async processing | | `NoContentResponseDirective` | 204 | Success, no response body | | `MovedPermanentlyResponseDirective` | 301 | Permanent redirect, `Location` | | `FoundResponseDirective` | 302 | Temporary redirect (common default) | | `SeeOtherResponseDirective` | 303 | Post/Redirect/GET pattern | | `TemporaryRedirectResponseDirective` | 307 | Temporary redirect, method preserved | | `PermanentRedirectResponseDirective` | 308 | Permanent redirect, method preserved | | `HeaderResponseDirective` | *(no change)* | Set or override an arbitrary response header | | `CookieResponseDirective` | *(no change)* | Set a cookie | All HTTP directives live in `edri.dataclass.directive.http`. ### HTML directives | Directive | HTTP status | When to use | |-----------|-------------|-------------| | `RedirectResponseDirective` | 302 | Redirect the browser to another page | `RedirectResponseDirective` is special: its `location` argument accepts either a string URL or an Event class (subclass of `Event`). When given an Event class, the framework resolves it to the URL declared in the event's `@api(url=…)` decorator, with optional keyword arguments for path placeholders. It lives in `edri.dataclass.directive.html`. --- ## How to return from a handler Directives are attached to `event.response` via `add_directive()`: ```python from edri.dataclass.directive.http import UnauthorizedResponseDirective event.response.set_status(ResponseStatus.FAILED) event.response.add_directive( UnauthorizedResponseDirective(message="Invalid token") ) ``` The first argument to every directive constructor is a `message` (where applicable). Additional keyword arguments are specific to the directive class. ### Redirect with event class resolution ```python from edri.dataclass.directive.html import RedirectResponseDirective event.response.add_directive( RedirectResponseDirective(ContactThanks) ) # Resolves ContactThanks to the URL in its @api(url=...) decorator event.response.add_directive( RedirectResponseDirective(UserPage, user_id=42) ) # Path placeholders can be filled via kwargs ``` ### Setting arbitrary headers ```python from edri.dataclass.directive.http import HeaderResponseDirective event.response.add_directive( HeaderResponseDirective(name="X-Custom", value="my-value") ) ``` ### Setting cookies ```python from edri.dataclass.directive.http import CookieResponseDirective event.response.add_directive( CookieResponseDirective( name="session", value="abc123", httponly=True, secure=True, path="/", max_age=3600, ) ) ``` --- ## How to inspect in tests The {class}`~edri.testing.ManagerTestHarness` provides {meth}`~edri.testing.ManagerTestHarness.get_directives` to verify directives without spawning child processes: ```python from edri.testing import ManagerTestHarness harness = ManagerTestHarness(MyManager(router_queue=mock_queue)) harness.prepare() event = MyEvent(...) harness.resolve(event) directives = harness.get_directives(event) assert len(directives) == 1 assert isinstance(directives[0], NotFoundResponseDirective) ``` The underlying method is `event.response.get_directives()`, which returns the raw list of {class}`~edri.dataclass.directive.ResponseDirective` instances attached to the response. --- ## HTTP status code resolution When the API layer processes a response, it determines the final HTTP status: 1. Start with **200 OK** (default). 2. Iterate through directives in order. 3. The last directive that defines a status wins. 4. `HeaderResponseDirective` and `CookieResponseDirective` do not change the status. This "last wins" behaviour applies per-response, not per-directive type — you can combine a cookie directive and a redirect directive in the same response.