@@ -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+
336379def _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