Skip to content

Keep the exception recorded by a Flask error handler on request.completed #35

Closed

Description

@anth-volk

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.completed record has outcome="error" and ERROR severity, but no error.type, error.message or error.stack fields;
  • 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.completed record with outcome="error" and the recorded exception's error.type, error.message and error.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, no error.* fields and no request error metric point.
  • A request that records a handled exception and returns a 4xx response keeps outcome="client_error" and no error.* fields.
  • An error argument passed to end_request still takes precedence over an attached exception.
  • FastAPI behavior for exceptions that propagate out of the application is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions