@@ -309,7 +309,16 @@ def response_hook(span: Span, status: str, response_headers: List):
309309
310310flask_version = version ("flask" )
311311
312- if package_version .parse (flask_version ) >= package_version .parse ("2.2.0" ):
312+ if package_version .parse (flask_version ) >= package_version .parse ("3.1.0" ):
313+ # Flask 3.1+ introduced changes to request context handling
314+ def _request_ctx_ref () -> weakref .ReferenceType :
315+ try :
316+ return weakref .ref (flask .globals .request_ctx ._get_current_object ())
317+ except (RuntimeError , AttributeError ):
318+ # Handle cases where request context is not available or has changed
319+ return weakref .ref (None )
320+
321+ elif package_version .parse (flask_version ) >= package_version .parse ("2.2.0" ):
313322
314323 def _request_ctx_ref () -> weakref .ReferenceType :
315324 return weakref .ref (flask .globals .request_ctx ._get_current_object ())
@@ -361,12 +370,12 @@ def _wrapped_app(wrapped_app_environ, start_response):
361370
362371 active_requests_counter .add (1 , active_requests_count_attrs )
363372 request_route = None
364-
365373 should_trace = True
366374
367375 def _start_response (status , response_headers , * args , ** kwargs ):
368376 nonlocal should_trace
369377 should_trace = _should_trace (excluded_urls )
378+
370379 if should_trace :
371380 nonlocal request_route
372381 request_route = flask .request .url_rule
@@ -407,35 +416,84 @@ def _start_response(status, response_headers, *args, **kwargs):
407416 response_hook (span , status , response_headers )
408417 return start_response (status , response_headers , * args , ** kwargs )
409418
410- result = wsgi_app (wrapped_app_environ , _start_response )
411- if should_trace :
412- duration_s = default_timer () - start
413- if duration_histogram_old :
414- duration_attrs_old = otel_wsgi ._parse_duration_attrs (
415- attributes , _StabilityMode .DEFAULT
416- )
419+ try :
420+ result = wsgi_app (wrapped_app_environ , _start_response )
417421
418- if request_route :
419- # http.target to be included in old semantic conventions
420- duration_attrs_old [HTTP_TARGET ] = str (request_route )
422+ # Handle streaming responses by ensuring proper cleanup
423+ is_streaming = (
424+ hasattr (result , '__iter__' ) and
425+ not isinstance (result , (bytes , str )) and
426+ hasattr (result , '__next__' )
427+ )
421428
422- duration_histogram_old . record (
423- max ( round ( duration_s * 1000 ), 0 ), duration_attrs_old
424- )
425- if duration_histogram_new :
426- duration_attrs_new = otel_wsgi . _parse_duration_attrs (
427- attributes , _StabilityMode . HTTP
428- )
429+ if is_streaming :
430+ # For streaming responses, defer cleanup until the response is consumed
431+ # We'll use a weakref callback or rely on the teardown handler
432+ pass
433+ else :
434+ # Non-streaming response, cleanup immediately
435+ _cleanup_context_safely ( wrapped_app_environ )
429436
430- if request_route :
431- duration_attrs_new [HTTP_ROUTE ] = str (request_route )
437+ if should_trace :
438+ duration_s = default_timer () - start
439+ if duration_histogram_old :
440+ duration_attrs_old = otel_wsgi ._parse_duration_attrs (
441+ attributes , _StabilityMode .DEFAULT
442+ )
443+
444+ if request_route :
445+ # http.target to be included in old semantic conventions
446+ duration_attrs_old [HTTP_TARGET ] = str (request_route )
447+
448+ duration_histogram_old .record (
449+ max (round (duration_s * 1000 ), 0 ), duration_attrs_old
450+ )
451+ if duration_histogram_new :
452+ duration_attrs_new = otel_wsgi ._parse_duration_attrs (
453+ attributes , _StabilityMode .HTTP
454+ )
455+
456+ if request_route :
457+ duration_attrs_new [HTTP_ROUTE ] = str (request_route )
458+
459+ duration_histogram_new .record (
460+ max (duration_s , 0 ), duration_attrs_new
461+ )
462+ except Exception :
463+ # Ensure cleanup on exception
464+ _cleanup_context_safely (wrapped_app_environ )
465+ raise
466+ finally :
467+ active_requests_counter .add (- 1 , active_requests_count_attrs )
432468
433- duration_histogram_new .record (
434- max (duration_s , 0 ), duration_attrs_new
435- )
436- active_requests_counter .add (- 1 , active_requests_count_attrs )
437469 return result
438470
471+ def _cleanup_context_safely (wrapped_app_environ ):
472+ """Clean up context and tokens safely"""
473+ try :
474+ # Clean up activation and token to prevent context leaks
475+ activation = wrapped_app_environ .get (_ENVIRON_ACTIVATION_KEY )
476+ token = wrapped_app_environ .get (_ENVIRON_TOKEN )
477+
478+ if activation and hasattr (activation , '__exit__' ):
479+ try :
480+ activation .__exit__ (None , None , None )
481+ except Exception :
482+ _logger .debug ("Failed to exit activation during context cleanup" , exc_info = True )
483+
484+ if token :
485+ try :
486+ context .detach (token )
487+ except Exception :
488+ _logger .debug ("Failed to detach token during context cleanup" , exc_info = True )
489+
490+ # Clean up environment keys
491+ for key in [_ENVIRON_ACTIVATION_KEY , _ENVIRON_TOKEN , _ENVIRON_SPAN_KEY , _ENVIRON_REQCTX_REF_KEY ]:
492+ wrapped_app_environ .pop (key , None )
493+
494+ except Exception :
495+ _logger .debug ("Exception during context cleanup" , exc_info = True )
496+
439497 def _should_trace (excluded_urls ) -> bool :
440498 return bool (
441499 flask .request
@@ -537,12 +595,24 @@ def _teardown_request(exc):
537595 return
538596
539597 activation = flask .request .environ .get (_ENVIRON_ACTIVATION_KEY )
598+ token = flask .request .environ .get (_ENVIRON_TOKEN )
599+
600+ # Check if this is a response that has already been cleaned up
601+ if not activation and not token :
602+ # Already cleaned up by streaming response handler
603+ return
540604
541605 original_reqctx_ref = flask .request .environ .get (
542606 _ENVIRON_REQCTX_REF_KEY
543607 )
544- current_reqctx_ref = _request_ctx_ref ()
545- if not activation or original_reqctx_ref != current_reqctx_ref :
608+
609+ try :
610+ current_reqctx_ref = _request_ctx_ref ()
611+ except (RuntimeError , AttributeError ):
612+ # Flask 3.1+ might raise exceptions when context is not available
613+ current_reqctx_ref = None
614+
615+ if not activation or (original_reqctx_ref and original_reqctx_ref != current_reqctx_ref ):
546616 # This request didn't start a span, maybe because it was created in
547617 # a way that doesn't run `before_request`, like when it is created
548618 # with `app.test_request_context`.
@@ -554,15 +624,28 @@ def _teardown_request(exc):
554624 # like any decorated with `flask.copy_current_request_context`.
555625
556626 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- )
563627
564- if flask .request .environ .get (_ENVIRON_TOKEN , None ):
565- context .detach (flask .request .environ .get (_ENVIRON_TOKEN ))
628+ try :
629+ if exc is None :
630+ activation .__exit__ (None , None , None )
631+ else :
632+ activation .__exit__ (
633+ type (exc ), exc , getattr (exc , "__traceback__" , None )
634+ )
635+ except Exception as e :
636+ _logger .debug ("Failed to exit activation in teardown" , exc_info = e )
637+
638+ try :
639+ if token :
640+ context .detach (token )
641+ except Exception as e :
642+ _logger .debug ("Failed to detach context in teardown" , exc_info = e )
643+
644+ # Clean up environment keys to prevent memory leaks
645+ flask .request .environ .pop (_ENVIRON_ACTIVATION_KEY , None )
646+ flask .request .environ .pop (_ENVIRON_TOKEN , None )
647+ flask .request .environ .pop (_ENVIRON_SPAN_KEY , None )
648+ flask .request .environ .pop (_ENVIRON_REQCTX_REF_KEY , None )
566649
567650 return _teardown_request
568651
0 commit comments