@@ -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,71 @@ 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 (
348+ hasattr (flask , "__version__" )
349+ and package_version .parse (flask .__version__ )
350+ >= package_version .parse ("3.1.0" )
351+ ):
352+ return
353+
354+ # Only enable streaming context cleanup for Python 3.10+ to avoid compatibility issues
355+ # with older Python versions that have different context management behavior
356+ if sys .version_info < (3 , 10 ):
357+ return
358+
359+ activation = environ .get (_ENVIRON_ACTIVATION_KEY )
360+ token = environ .get (_ENVIRON_TOKEN )
361+
362+ if not activation or not token :
363+ return
364+
365+ # Additional safety check - only proceed if we haven't already cleaned up
366+ if (
367+ environ .get (_ENVIRON_ACTIVATION_KEY ) is None
368+ or environ .get (_ENVIRON_TOKEN ) is None
369+ ):
370+ return
371+
372+ try :
373+ # Clean up the context token safely
374+ if token :
375+ try :
376+ context .detach (token )
377+ except RuntimeError as exc :
378+ # Token has already been used - this can happen in Flask 3.1+
379+ # with streaming responses, so we just log and continue
380+ _logger .debug ("Token already detached, continuing: %s" , exc )
381+ # If detach failed, don't proceed with activation cleanup
382+ return
383+
384+ # Clean up the activation
385+ if hasattr (activation , "__exit__" ):
386+ try :
387+ activation .__exit__ (None , None , None )
388+ except (RuntimeError , AttributeError ) as exc :
389+ _logger .debug ("Error during activation cleanup: %s" , exc )
390+
391+ # Mark that we've handled the cleanup to prevent double cleanup in teardown
392+ environ [_ENVIRON_ACTIVATION_KEY ] = None
393+ environ [_ENVIRON_TOKEN ] = None
394+
395+ except (RuntimeError , ValueError , TypeError ) as exc :
396+ # Log the error but don't raise it to avoid breaking the response
397+ _logger .debug (
398+ "Error during streaming context cleanup: %s" , exc , exc_info = True
399+ )
400+
401+
336402def _rewrapped_app (
337403 wsgi_app ,
338404 active_requests_counter ,
@@ -408,6 +474,46 @@ def _start_response(status, response_headers, *args, **kwargs):
408474 return start_response (status , response_headers , * args , ** kwargs )
409475
410476 result = wsgi_app (wrapped_app_environ , _start_response )
477+
478+ # For Flask 3.1+, check if we need to handle streaming response context cleanup
479+ # Only run this logic in Flask 3.1+ and Python 3.10+ to avoid any interference with older versions
480+ # Use very conservative checks to ensure we never interfere with Flask < 3.1
481+ if (
482+ should_trace
483+ and hasattr (
484+ flask , "__version__"
485+ ) # Ensure Flask has version attribute
486+ and package_version .parse (flask .__version__ )
487+ >= package_version .parse ("3.1.0" ) # Only Flask 3.1+
488+ and sys .version_info >= (3 , 10 ) # Only Python 3.10+
489+ ):
490+ # Only call streaming context cleanup for actual streaming responses
491+ # Add additional safety checks to ensure we're really in Flask 3.1+
492+ try :
493+ # Additional safety check: verify we're in a Flask request context
494+ if hasattr (flask , "request" ) and hasattr (
495+ flask .request , "response"
496+ ):
497+ is_streaming = (
498+ hasattr (flask .request , "response" )
499+ and flask .request .response
500+ and hasattr (flask .request .response , "stream" )
501+ and flask .request .response .stream
502+ )
503+ if is_streaming :
504+ _ensure_streaming_context_cleanup (wrapped_app_environ )
505+ except (
506+ RuntimeError ,
507+ ValueError ,
508+ TypeError ,
509+ AttributeError ,
510+ ) as exc :
511+ # Ensure our Flask 3.1+ logic never interferes with normal request processing
512+ _logger .debug (
513+ "Flask 3.1+ streaming context cleanup failed, continuing: %s" ,
514+ exc ,
515+ )
516+
411517 if should_trace :
412518 duration_s = default_timer () - start
413519 if duration_histogram_old :
@@ -433,6 +539,7 @@ def _start_response(status, response_headers, *args, **kwargs):
433539 duration_histogram_new .record (
434540 max (duration_s , 0 ), duration_attrs_new
435541 )
542+
436543 active_requests_counter .add (- 1 , active_requests_count_attrs )
437544 return result
438545
@@ -537,6 +644,7 @@ def _teardown_request(exc):
537644 return
538645
539646 activation = flask .request .environ .get (_ENVIRON_ACTIVATION_KEY )
647+ token = flask .request .environ .get (_ENVIRON_TOKEN )
540648
541649 original_reqctx_ref = flask .request .environ .get (
542650 _ENVIRON_REQCTX_REF_KEY
@@ -554,15 +662,63 @@ def _teardown_request(exc):
554662 # like any decorated with `flask.copy_current_request_context`.
555663
556664 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 )
665+
666+ try :
667+ # For Flask 3.1+, check if this is a streaming response that might
668+ # have already been cleaned up to prevent double cleanup
669+ # Only check for streaming in Flask 3.1+ and Python 3.10+ to avoid interference with older versions
670+ # Use very conservative checks to ensure we never interfere with Flask < 3.1
671+ is_flask_31_plus = (
672+ hasattr (
673+ flask , "__version__"
674+ ) # Ensure Flask has version attribute
675+ and package_version .parse (flask .__version__ )
676+ >= package_version .parse ("3.1.0" )
677+ and sys .version_info >= (3 , 10 )
562678 )
563679
564- if flask .request .environ .get (_ENVIRON_TOKEN , None ):
565- context .detach (flask .request .environ .get (_ENVIRON_TOKEN ))
680+ is_streaming = False
681+ if is_flask_31_plus :
682+ try :
683+ # Additional safety check: verify we're in a Flask request context
684+ if hasattr (flask , "request" ) and hasattr (
685+ flask .request , "response"
686+ ):
687+ is_streaming = (
688+ hasattr (flask .request , "response" )
689+ and flask .request .response
690+ and hasattr (flask .request .response , "stream" )
691+ and flask .request .response .stream
692+ )
693+ except (RuntimeError , AttributeError ):
694+ # Not in a proper Flask request context, don't check for streaming
695+ is_streaming = False
696+
697+ if is_flask_31_plus and is_streaming :
698+ # For streaming responses in Flask 3.1+, the context might have been
699+ # cleaned up already in _ensure_streaming_context_cleanup
700+ # Mark the activation and token as None to prevent double cleanup
701+ flask .request .environ [_ENVIRON_ACTIVATION_KEY ] = None
702+ flask .request .environ [_ENVIRON_TOKEN ] = None
703+ return
704+
705+ if exc is None :
706+ activation .__exit__ (None , None , None )
707+ else :
708+ activation .__exit__ (
709+ type (exc ), exc , getattr (exc , "__traceback__" , None )
710+ )
711+
712+ if token :
713+ context .detach (token )
714+
715+ except (RuntimeError , AttributeError , ValueError ) as teardown_exc :
716+ # Log the error but don't raise it to avoid breaking the request handling
717+ _logger .debug (
718+ "Error during request teardown: %s" ,
719+ teardown_exc ,
720+ exc_info = True ,
721+ )
566722
567723 return _teardown_request
568724
0 commit comments