Skip to content

Commit de0b99a

Browse files
committed
chore: compatiable flask 3.1+
1 parent bd3c1f2 commit de0b99a

File tree

2 files changed

+442
-7
lines changed

2 files changed

+442
-7
lines changed

instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,49 @@ def get_default_span_name():
333333
return span_name
334334

335335

336+
def _ensure_streaming_context_cleanup(environ):
337+
"""
338+
Ensure proper context cleanup for streaming responses in Flask 3.1+.
339+
340+
This function checks if the response is a streaming response and ensures
341+
that context tokens are properly cleaned up to prevent token reuse issues.
342+
"""
343+
activation = environ.get(_ENVIRON_ACTIVATION_KEY)
344+
token = environ.get(_ENVIRON_TOKEN)
345+
346+
if not activation or not token:
347+
return
348+
349+
try:
350+
# Check if we have a Flask request context and if this might be a streaming response
351+
# For Flask 3.1+, we need to be more proactive about context cleanup
352+
# because streaming responses can cause token reuse issues
353+
354+
# Mark that we've handled the cleanup to prevent double cleanup in teardown
355+
environ[_ENVIRON_ACTIVATION_KEY] = None
356+
environ[_ENVIRON_TOKEN] = None
357+
358+
# Clean up the context token safely
359+
if token:
360+
try:
361+
context.detach(token)
362+
except RuntimeError as e:
363+
# Token has already been used - this can happen in Flask 3.1+
364+
# with streaming responses, so we just log and continue
365+
_logger.debug("Token already detached, continuing: %s", e)
366+
367+
# Clean up the activation
368+
if hasattr(activation, "__exit__"):
369+
try:
370+
activation.__exit__(None, None, None)
371+
except Exception as e:
372+
_logger.debug("Error during activation cleanup: %s", e)
373+
374+
except Exception:
375+
# Log the error but don't raise it to avoid breaking the response
376+
_logger.debug("Error during streaming context cleanup", exc_info=True)
377+
378+
336379
def _rewrapped_app(
337380
wsgi_app,
338381
active_requests_counter,
@@ -408,6 +451,13 @@ def _start_response(status, response_headers, *args, **kwargs):
408451
return start_response(status, response_headers, *args, **kwargs)
409452

410453
result = wsgi_app(wrapped_app_environ, _start_response)
454+
455+
# For Flask 3.1+, check if we need to handle streaming response context cleanup
456+
if should_trace and package_version.parse(
457+
flask_version
458+
) >= package_version.parse("3.1.0"):
459+
_ensure_streaming_context_cleanup(wrapped_app_environ)
460+
411461
if should_trace:
412462
duration_s = default_timer() - start
413463
if duration_histogram_old:
@@ -433,6 +483,7 @@ def _start_response(status, response_headers, *args, **kwargs):
433483
duration_histogram_new.record(
434484
max(duration_s, 0), duration_attrs_new
435485
)
486+
436487
active_requests_counter.add(-1, active_requests_count_attrs)
437488
return result
438489

@@ -537,6 +588,7 @@ def _teardown_request(exc):
537588
return
538589

539590
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
591+
token = flask.request.environ.get(_ENVIRON_TOKEN)
540592

541593
original_reqctx_ref = flask.request.environ.get(
542594
_ENVIRON_REQCTX_REF_KEY
@@ -554,15 +606,42 @@ def _teardown_request(exc):
554606
# like any decorated with `flask.copy_current_request_context`.
555607

556608
return
557-
if exc is None:
558-
activation.__exit__(None, None, None)
559-
else:
560-
activation.__exit__(
561-
type(exc), exc, getattr(exc, "__traceback__", None)
609+
610+
try:
611+
# For Flask 3.1+, check if this is a streaming response that might
612+
# have already been cleaned up to prevent double cleanup
613+
is_streaming = (
614+
hasattr(flask.request, "response")
615+
and flask.request.response
616+
and hasattr(flask.request.response, "stream")
617+
and flask.request.response.stream
562618
)
563619

564-
if flask.request.environ.get(_ENVIRON_TOKEN, None):
565-
context.detach(flask.request.environ.get(_ENVIRON_TOKEN))
620+
if (
621+
package_version.parse(flask_version)
622+
>= package_version.parse("3.1.0")
623+
and is_streaming
624+
):
625+
# For streaming responses in Flask 3.1+, the context might have been
626+
# cleaned up already in _ensure_streaming_context_cleanup
627+
# Mark the activation and token as None to prevent double cleanup
628+
flask.request.environ[_ENVIRON_ACTIVATION_KEY] = None
629+
flask.request.environ[_ENVIRON_TOKEN] = None
630+
return
631+
632+
if exc is None:
633+
activation.__exit__(None, None, None)
634+
else:
635+
activation.__exit__(
636+
type(exc), exc, getattr(exc, "__traceback__", None)
637+
)
638+
639+
if token:
640+
context.detach(token)
641+
642+
except Exception:
643+
# Log the error but don't raise it to avoid breaking the request handling
644+
_logger.debug("Error during request teardown", exc_info=True)
566645

567646
return _teardown_request
568647

0 commit comments

Comments
 (0)