Troubleshooting¶
Common problems and their solutions. If your issue is not here, check the logs — EDRI logs at DEBUG level by default, and most components emit detailed messages.
Event not delivered to Manager¶
Symptom: You send an event to the router queue but the Manager's @request handler is never called.
Causes and fixes:
Manager not registered — verify you called
edri.add_component(MyManager(edri.router_queue))beforeedri.run().Handler type annotation missing or wrong —
_prepare_resolvers()reads the type annotation of the first parameter, which must be namedeventand annotated with anEventsubclass. If the parameter is unnamed/misnamed or the annotation is missing or not anEventsubclass,_prepare_resolvers()raisesAttributeErrorat startup and the Manager process exits before its event loop starts (so it never handles anything).# Wrong — missing annotation @request def handle(self, event) -> None: ... # Correct @request def handle(self, event: MyEvent) -> None: ...
Event type mismatch — the Router dispatches by exact Python type. If you put a subclass on the queue but the Manager subscribes to the parent class, the subclass event will not match. Subscribe to the exact type you emit, or subscribe to each subclass separately.
Manager crashed on startup — if
after_start()raises an exception, the Manager may exit before entering its event loop. Check the process stderr or configure logging.from_timefiltering — if the Manager was started withfrom_time=datetime(...), events timestamped before that datetime are silently discarded. This is intentional for replaying missed events after reconnect.
pickle error when putting event on queue¶
Symptom: _pickle.PicklingError: Can't pickle ... or similar.
Cause: An event field holds a value that cannot be pickled. Common culprits:
Lambda functions as field defaults or values.
File handles, socket objects, database connections.
Generator objects.
Locally-defined classes that Python cannot resolve.
Fix: Replace unpicklable values with picklable representations:
# Wrong
@event
class Task(Event):
callback: Callable # lambda is not picklable
# Correct — pass the data, call the function on the receiver side
@event
class Task(Event):
data: dict
For file content, use SharedMemoryPipe (see Downloading files) rather than passing file handles directly.
Response never reaches the API client (request times out)¶
Symptom: REST or WebSocket request hangs and eventually times out (EDRI_API_RESPONSE_TIMEOUT).
Checklist:
Handler does not modify
event.response— if no field is set andset_status()is never called,has_response()remainsFalseand the auto-send is skipped. The API layer waits indefinitely.Add at minimum:
event.response.set_status(ResponseStatus.OK)
Worker forgets to call
event_send(event)— unlike Manager handlers, Workers do not auto-send. If a Worker modifiesevent.responsebut omitsself.event_send(event), the Manager never receives the response.Manager emits a new event instead of responding — if the handler puts a new event on
router_queuebut does not setevent.response, the original request has no response.Exception in handler — if the handler raises an unhandled exception, the event lifecycle stops (the caller gets no response). The Manager itself does not crash: the exception is captured and surfaced on the next health cycle as
WARNINGin Status endpoints. Enable DEBUG logging to see the traceback, or wrap handler bodies in try/except and set an error directive if you want a proper client response:from edri.dataclass.directive.base import InternalServerErrorResponseDirective @request def handle(self, event: MyEvent) -> None: try: ... except Exception as e: self.logger.exception("Unhandled error", exc_info=e) event.response.set_status(ResponseStatus.FAILED) event.response.add_directive(InternalServerErrorResponseDirective(str(e)))
REST endpoint returns HTML instead of JSON¶
Symptom: A curl request receives HTML when JSON is expected.
Fix: Add Accept: application/json to the request:
curl -H "Accept: application/json" http://localhost:8080/items
Without the Accept header, or with Accept: */*, the Listener may prefer the HTML renderer if an HTML endpoint exists at the same URL. See Content Negotiation.
Validation returns 400 but error message is unclear¶
Symptom: A validation failure returns HTTP 400, but the body shows only one reason (or a terse exception string) and you can't tell what really failed.
Explanation: The 400 body carries the validation failures as a reasons list. How much of it is exposed is controlled by EDRI_API_EXPOSE_ERROR_DETAILS, which defaults to the runtime mode (ENVIRONMENT):
development (
true) — the full chain of reasons is returned, including the raw exception strings and the field that failed. Good for debugging.production (
false) — thereasonslist is truncated to the first entry, so internal details are not leaked. With several fields failing at once you only see the first one — which is why a production 400 can look "unclear".
So a message that is detailed in dev but terse in prod is expected behavior, not a bug. To get the full detail in a given environment, set the toggle explicitly:
export EDRI_API_EXPOSE_ERROR_DETAILS=true
The full (development) format looks like:
Error 400: Cannot create <EventType>
ValueError("Wrong type ...")
The field name that failed validation is included in the reason. To reshape error bodies beyond this toggle (a custom schema, masking), use a response Middleware. See Environment Variables.
WebSocket client does not receive server-push events¶
Symptom: A Manager puts a server-push event on the router queue but the WS client never receives it.
Checklist:
Client did not register — the client must send a
registercommand before it will receive subscribed events. See WebSocket.param_setmismatch — the client registered with{"project_id": 10}but the emitted event hasproject_id=11. Values must match exactly.Wrong
handlingtype — the event's@api(handling=...)must includeSUBSCRIBEDfor subscription-based delivery. Ifhandling=EventHandlingType.SPECIFIC, only the originating client receives it.Event not in
api_events— the Broker only forwards events defined with@api(...). Fire-and-forget internal events are not forwarded to WS clients.Broker can't route it — enable DEBUG logging on the Broker (
edri.api.broker) to trace event routing:import logging logging.getLogger("edri.api.broker").setLevel(logging.DEBUG)
HealthChecker marks Manager as WARNING or ERROR¶
Symptom: Log shows Manager X: WARNING or Manager X: ERROR.
Meaning: The Manager missed one or more health-check pings. EDRI_HEALTH_CHECK_FAILURE_LIMIT missed pings (default: 3) triggers ERROR.
Checklist:
Manager is deadlocked — if the Manager's handler calls something that blocks (network I/O,
time.sleep, a lock), subsequent events — including health checks — queue up and appear as missed.Fix: move blocking work to a Worker (thread or process).
Worker limit reached — if
max_workersis set and all slots are full, new events including health checks are processed only after a worker finishes.Manager process exited — check if the Manager raised an unhandled exception. Enable logging at DEBUG level.
Adjusting sensitivity:
# Check every 30 seconds, allow 5 missed pings before ERROR
export EDRI_HEALTH_CHECK_TIMEOUT=30
export EDRI_HEALTH_CHECK_FAILURE_LIMIT=5
Scheduler job does not fire¶
Symptom: A scheduled job never fires even though when is in the past.
Checklist:
Scheduler not started — confirm
edri.start_scheduler()is in your startup code.Timezone mismatch — if you pass a naive
datetime(notzinfo), EDRI converts it to local time. If the system timezone is not what you expect, the job fires at the wrong absolute time. Use timezone-aware datetimes:from datetime import datetime, timezone when = datetime.now(tz=timezone.utc) + timedelta(minutes=5)
repeatanchoring — if the system is overloaded and a job fires late,whenis still advanced by the originalrepeatinterval, not from the actual firing time. This prevents drift but means a very delayed job may fire again almost immediately.
Connector fails to reconnect to the Switch¶
Symptom: Log shows repeated Connection refused or Connection reset messages for the Switch.
Checklist:
Switch not running — start the Switch with
python -m edri.switch.switchor via Docker.Wrong host/port — check
EDRI_SWITCH_HOSTandEDRI_SWITCH_PORTmatch the Switch's bind address.Firewall — ensure the port is open between the EDRI instance and the Switch host.
Events accumulate in cache — while disconnected, the Router stores events for up to
EDRI_CACHE_TIMEOUTseconds. Events older than the timeout are dropped. IncreaseEDRI_CACHE_TIMEOUTif short network interruptions are causing event loss.
mypy type errors in Manager or Worker subclasses¶
Symptom: mypy reports errors about @request / @response decorated methods or unresolved types.
Notes:
ManagerBaseMetarenames decorated methods at class creation time (handle → solve_req_handle). This confuses static analysis tools that do not understand metaclass transformations.The project runs mypy in strict mode (configured in
pyproject.toml). Theedri/switch/subtree is excluded.Type-annotate handler parameters explicitly — mypy needs the annotation to resolve event types.
If you use
inject(...)as a field type, mypy may not resolve the return type automatically. Add an explicit cast or use# type: ignoreonly on that line.
See also¶
Deployment — production configuration checklist
Environment Variables — all environment variables
Manager — Manager lifecycle,
after_start(), cachingWorker — Worker communication,
event_send()Middleware — middleware execution order
Specialized managers — Scheduler and Store behavior