@@ -254,6 +254,7 @@ def response_hook(span: Span, status: str, response_headers: List):
254254---
255255"""
256256
257+ import sys
257258import weakref
258259from logging import getLogger
259260from time import time_ns
@@ -333,6 +334,66 @@ def get_default_span_name():
333334 return span_name
334335
335336
337+ def _ensure_streaming_context_cleanup (environ ):
338+ """
339+ Ensure proper context cleanup for streaming responses in Flask 3.1+.
340+
341+ This function checks if the response is a streaming response and ensures
342+ that context tokens are properly cleaned up to prevent token reuse issues.
343+ Only applies to Flask 3.1+ and Python 3.10+ for compatibility reasons.
344+ """
345+ # Double-check Flask version - this should only run in Flask 3.1+
346+ if package_version .parse (flask_version ) < package_version .parse ("3.1.0" ):
347+ return
348+
349+ # Only enable streaming context cleanup for Python 3.10+ to avoid compatibility issues
350+ # with older Python versions that have different context management behavior
351+ if sys .version_info < (3 , 10 ):
352+ return
353+
354+ activation = environ .get (_ENVIRON_ACTIVATION_KEY )
355+ token = environ .get (_ENVIRON_TOKEN )
356+
357+ if not activation or not token :
358+ return
359+
360+ # Additional safety check - only proceed if we haven't already cleaned up
361+ if (
362+ environ .get (_ENVIRON_ACTIVATION_KEY ) is None
363+ or environ .get (_ENVIRON_TOKEN ) is None
364+ ):
365+ return
366+
367+ try :
368+ # Clean up the context token safely
369+ if token :
370+ try :
371+ context .detach (token )
372+ except RuntimeError as exc :
373+ # Token has already been used - this can happen in Flask 3.1+
374+ # with streaming responses, so we just log and continue
375+ _logger .debug ("Token already detached, continuing: %s" , exc )
376+ # If detach failed, don't proceed with activation cleanup
377+ return
378+
379+ # Clean up the activation
380+ if hasattr (activation , "__exit__" ):
381+ try :
382+ activation .__exit__ (None , None , None )
383+ except (RuntimeError , AttributeError ) as exc :
384+ _logger .debug ("Error during activation cleanup: %s" , exc )
385+
386+ # Mark that we've handled the cleanup to prevent double cleanup in teardown
387+ environ [_ENVIRON_ACTIVATION_KEY ] = None
388+ environ [_ENVIRON_TOKEN ] = None
389+
390+ except (RuntimeError , ValueError , TypeError ) as exc :
391+ # Log the error but don't raise it to avoid breaking the response
392+ _logger .debug (
393+ "Error during streaming context cleanup: %s" , exc , exc_info = True
394+ )
395+
396+
336397def _rewrapped_app (
337398 wsgi_app ,
338399 active_requests_counter ,
@@ -408,6 +469,37 @@ def _start_response(status, response_headers, *args, **kwargs):
408469 return start_response (status , response_headers , * args , ** kwargs )
409470
410471 result = wsgi_app (wrapped_app_environ , _start_response )
472+
473+ # For Flask 3.1+, check if we need to handle streaming response context cleanup
474+ # Only run this logic in Flask 3.1+ and Python 3.10+ to avoid any interference with older versions
475+ if (
476+ should_trace
477+ and package_version .parse (flask_version )
478+ >= package_version .parse ("3.1.0" )
479+ and sys .version_info >= (3 , 10 )
480+ ):
481+ # Only call streaming context cleanup for actual streaming responses
482+ is_streaming = (
483+ hasattr (flask .request , "response" )
484+ and flask .request .response
485+ and hasattr (flask .request .response , "stream" )
486+ and flask .request .response .stream
487+ )
488+ if is_streaming :
489+ try :
490+ _ensure_streaming_context_cleanup (wrapped_app_environ )
491+ except (
492+ RuntimeError ,
493+ ValueError ,
494+ TypeError ,
495+ AttributeError ,
496+ ) as exc :
497+ # Ensure our Flask 3.1+ logic never interferes with normal request processing
498+ _logger .debug (
499+ "Flask 3.1+ streaming context cleanup failed, continuing: %s" ,
500+ exc ,
501+ )
502+
411503 if should_trace :
412504 duration_s = default_timer () - start
413505 if duration_histogram_old :
@@ -433,6 +525,7 @@ def _start_response(status, response_headers, *args, **kwargs):
433525 duration_histogram_new .record (
434526 max (duration_s , 0 ), duration_attrs_new
435527 )
528+
436529 active_requests_counter .add (- 1 , active_requests_count_attrs )
437530 return result
438531
@@ -537,6 +630,7 @@ def _teardown_request(exc):
537630 return
538631
539632 activation = flask .request .environ .get (_ENVIRON_ACTIVATION_KEY )
633+ token = flask .request .environ .get (_ENVIRON_TOKEN )
540634
541635 original_reqctx_ref = flask .request .environ .get (
542636 _ENVIRON_REQCTX_REF_KEY
@@ -554,15 +648,49 @@ def _teardown_request(exc):
554648 # like any decorated with `flask.copy_current_request_context`.
555649
556650 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 )
562- )
563651
564- if flask .request .environ .get (_ENVIRON_TOKEN , None ):
565- context .detach (flask .request .environ .get (_ENVIRON_TOKEN ))
652+ try :
653+ # For Flask 3.1+, check if this is a streaming response that might
654+ # have already been cleaned up to prevent double cleanup
655+ # Only check for streaming in Flask 3.1+ and Python 3.10+ to avoid interference with older versions
656+ is_flask_31_plus = package_version .parse (
657+ flask_version
658+ ) >= package_version .parse ("3.1.0" ) and sys .version_info >= (3 , 10 )
659+
660+ is_streaming = False
661+ if is_flask_31_plus :
662+ is_streaming = (
663+ hasattr (flask .request , "response" )
664+ and flask .request .response
665+ and hasattr (flask .request .response , "stream" )
666+ and flask .request .response .stream
667+ )
668+
669+ if is_flask_31_plus and is_streaming :
670+ # For streaming responses in Flask 3.1+, the context might have been
671+ # cleaned up already in _ensure_streaming_context_cleanup
672+ # Mark the activation and token as None to prevent double cleanup
673+ flask .request .environ [_ENVIRON_ACTIVATION_KEY ] = None
674+ flask .request .environ [_ENVIRON_TOKEN ] = None
675+ return
676+
677+ if exc is None :
678+ activation .__exit__ (None , None , None )
679+ else :
680+ activation .__exit__ (
681+ type (exc ), exc , getattr (exc , "__traceback__" , None )
682+ )
683+
684+ if token :
685+ context .detach (token )
686+
687+ except (RuntimeError , AttributeError , ValueError ) as teardown_exc :
688+ # Log the error but don't raise it to avoid breaking the request handling
689+ _logger .debug (
690+ "Error during request teardown: %s" ,
691+ teardown_exc ,
692+ exc_info = True ,
693+ )
566694
567695 return _teardown_request
568696
0 commit comments