@@ -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,68 @@ 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+ # Use the same check method as other places for consistency
347+ if not (hasattr (flask , '__version__' ) and
348+ package_version .parse (flask .__version__ ) >= package_version .parse ("3.1.0" )):
349+ return
350+
351+ # Only enable streaming context cleanup for Python 3.10+ to avoid compatibility issues
352+ # with older Python versions that have different context management behavior
353+ if sys .version_info < (3 , 10 ):
354+ return
355+
356+ activation = environ .get (_ENVIRON_ACTIVATION_KEY )
357+ token = environ .get (_ENVIRON_TOKEN )
358+
359+ if not activation or not token :
360+ return
361+
362+ # Additional safety check - only proceed if we haven't already cleaned up
363+ if (
364+ environ .get (_ENVIRON_ACTIVATION_KEY ) is None
365+ or environ .get (_ENVIRON_TOKEN ) is None
366+ ):
367+ return
368+
369+ try :
370+ # Clean up the context token safely
371+ if token :
372+ try :
373+ context .detach (token )
374+ except RuntimeError as exc :
375+ # Token has already been used - this can happen in Flask 3.1+
376+ # with streaming responses, so we just log and continue
377+ _logger .debug ("Token already detached, continuing: %s" , exc )
378+ # If detach failed, don't proceed with activation cleanup
379+ return
380+
381+ # Clean up the activation
382+ if hasattr (activation , "__exit__" ):
383+ try :
384+ activation .__exit__ (None , None , None )
385+ except (RuntimeError , AttributeError ) as exc :
386+ _logger .debug ("Error during activation cleanup: %s" , exc )
387+
388+ # Mark that we've handled the cleanup to prevent double cleanup in teardown
389+ environ [_ENVIRON_ACTIVATION_KEY ] = None
390+ environ [_ENVIRON_TOKEN ] = None
391+
392+ except (RuntimeError , ValueError , TypeError ) as exc :
393+ # Log the error but don't raise it to avoid breaking the response
394+ _logger .debug (
395+ "Error during streaming context cleanup: %s" , exc , exc_info = True
396+ )
397+
398+
336399def _rewrapped_app (
337400 wsgi_app ,
338401 active_requests_counter ,
@@ -408,6 +471,43 @@ def _start_response(status, response_headers, *args, **kwargs):
408471 return start_response (status , response_headers , * args , ** kwargs )
409472
410473 result = wsgi_app (wrapped_app_environ , _start_response )
474+
475+ # For Flask 3.1+, check if we need to handle streaming response context cleanup
476+ # Only run this logic in Flask 3.1+ and Python 3.10+ to avoid any interference with older versions
477+ # Use very conservative checks to ensure we never interfere with Flask < 3.1
478+ if (
479+ should_trace
480+ and hasattr (flask , '__version__' ) # Ensure Flask has version attribute
481+ and package_version .parse (flask .__version__ )
482+ >= package_version .parse ("3.1.0" ) # Only Flask 3.1+
483+ and sys .version_info >= (3 , 10 ) # Only Python 3.10+
484+ ):
485+ # Only call streaming context cleanup for actual streaming responses
486+ # Add additional safety checks to ensure we're really in Flask 3.1+
487+ try :
488+ # Additional safety check: verify we're in a Flask request context
489+ if (hasattr (flask , 'request' ) and
490+ hasattr (flask .request , 'response' )):
491+ is_streaming = (
492+ hasattr (flask .request , "response" )
493+ and flask .request .response
494+ and hasattr (flask .request .response , "stream" )
495+ and flask .request .response .stream
496+ )
497+ if is_streaming :
498+ _ensure_streaming_context_cleanup (wrapped_app_environ )
499+ except (
500+ RuntimeError ,
501+ ValueError ,
502+ TypeError ,
503+ AttributeError ,
504+ ) as exc :
505+ # Ensure our Flask 3.1+ logic never interferes with normal request processing
506+ _logger .debug (
507+ "Flask 3.1+ streaming context cleanup failed, continuing: %s" ,
508+ exc ,
509+ )
510+
411511 if should_trace :
412512 duration_s = default_timer () - start
413513 if duration_histogram_old :
@@ -433,6 +533,7 @@ def _start_response(status, response_headers, *args, **kwargs):
433533 duration_histogram_new .record (
434534 max (duration_s , 0 ), duration_attrs_new
435535 )
536+
436537 active_requests_counter .add (- 1 , active_requests_count_attrs )
437538 return result
438539
@@ -537,6 +638,7 @@ def _teardown_request(exc):
537638 return
538639
539640 activation = flask .request .environ .get (_ENVIRON_ACTIVATION_KEY )
641+ token = flask .request .environ .get (_ENVIRON_TOKEN )
540642
541643 original_reqctx_ref = flask .request .environ .get (
542644 _ENVIRON_REQCTX_REF_KEY
@@ -554,15 +656,59 @@ def _teardown_request(exc):
554656 # like any decorated with `flask.copy_current_request_context`.
555657
556658 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 )
659+
660+ try :
661+ # For Flask 3.1+, check if this is a streaming response that might
662+ # have already been cleaned up to prevent double cleanup
663+ # Only check for streaming in Flask 3.1+ and Python 3.10+ to avoid interference with older versions
664+ # Use very conservative checks to ensure we never interfere with Flask < 3.1
665+ is_flask_31_plus = (
666+ hasattr (flask , '__version__' ) # Ensure Flask has version attribute
667+ and package_version .parse (flask .__version__ ) >= package_version .parse ("3.1.0" )
668+ and sys .version_info >= (3 , 10 )
562669 )
563670
564- if flask .request .environ .get (_ENVIRON_TOKEN , None ):
565- context .detach (flask .request .environ .get (_ENVIRON_TOKEN ))
671+ is_streaming = False
672+ if is_flask_31_plus :
673+ try :
674+ # Additional safety check: verify we're in a Flask request context
675+ if (hasattr (flask , 'request' ) and
676+ hasattr (flask .request , 'response' )):
677+ is_streaming = (
678+ hasattr (flask .request , "response" )
679+ and flask .request .response
680+ and hasattr (flask .request .response , "stream" )
681+ and flask .request .response .stream
682+ )
683+ except (RuntimeError , AttributeError ):
684+ # Not in a proper Flask request context, don't check for streaming
685+ is_streaming = False
686+
687+ if is_flask_31_plus and is_streaming :
688+ # For streaming responses in Flask 3.1+, the context might have been
689+ # cleaned up already in _ensure_streaming_context_cleanup
690+ # Mark the activation and token as None to prevent double cleanup
691+ flask .request .environ [_ENVIRON_ACTIVATION_KEY ] = None
692+ flask .request .environ [_ENVIRON_TOKEN ] = None
693+ return
694+
695+ if exc is None :
696+ activation .__exit__ (None , None , None )
697+ else :
698+ activation .__exit__ (
699+ type (exc ), exc , getattr (exc , "__traceback__" , None )
700+ )
701+
702+ if token :
703+ context .detach (token )
704+
705+ except (RuntimeError , AttributeError , ValueError ) as teardown_exc :
706+ # Log the error but don't raise it to avoid breaking the request handling
707+ _logger .debug (
708+ "Error during request teardown: %s" ,
709+ teardown_exc ,
710+ exc_info = True ,
711+ )
566712
567713 return _teardown_request
568714
0 commit comments