Repository navigation
Keep the exception recorded by a Flask error handler on request.completed #35
Description
Problem
ObservabilityRuntime.record_exception(error, handled=..., status_code=...) attaches the exception to the active request state and emits an exception.recorded record. ObservabilityRuntime.end_request then assigns its own error argument to the request state unconditionally (state.error = error), so a call without an error argument replaces the attached exception with None.
The Flask adapter ends the request from its teardown_request callback with runtime.end_request(status_code=500 if error is not None else None, error=error). When a registered Flask error handler has handled the exception, Flask passes None to teardown callbacks. The attached exception is therefore discarded before the completion record is built.
_finish_operation already keeps an attached exception for operations (state.error = error or state.error); requests do not.
Effect
For a Flask application whose error handler calls record_exception(error, handled=False, status_code=500) and returns a 500 response:
- the
request.completedrecord hasoutcome="error"and ERROR severity, but noerror.type,error.messageorerror.stackfields; - the server span is marked with error status but is ended without the exception event.
The exception details are only available on the separate exception.recorded record. The Public API (PolicyEngine/policyengine-api#3984) hit this when its Flask catch-all error handler started calling record_exception.
The FastAPI adapter passes an exception that propagates out of the application to end_request directly, so that path is not affected. A FastAPI exception handler registered for a specific exception type that records the exception and returns a 500 response has the same problem as Flask, because the adapter only sees the response.
Proposed change
In end_request, keep computing the outcome from the error argument and the resolved status code exactly as now. Only when that outcome is "error" and no error argument was passed, use the exception attached by record_exception for the request.completed error fields and for ending the span.
The fix must not use the attached exception for every request. Applications call record_exception(..., handled=True) for exceptions they catch before returning a 2xx or 4xx response. If the attached exception were included when computing the outcome, those requests would become outcome="error", their request.completed record would become ERROR severity, and they would add a request point to policyengine.error.count.
Acceptance criteria
- A Flask error handler that records an exception and returns a 500 response produces exactly one
request.completedrecord withoutcome="error"and the recorded exception'serror.type,error.messageanderror.stack, and the span carries the exception event. - A request that records a handled exception and returns a 2xx response keeps
outcome="success", INFO severity, noerror.*fields and no request error metric point. - A request that records a handled exception and returns a 4xx response keeps
outcome="client_error"and noerror.*fields. - An
errorargument passed toend_requeststill takes precedence over an attached exception. - FastAPI behavior for exceptions that propagate out of the application is unchanged.
Activity