Skip to content

Commit 565e22c

Browse files
committed
fix: preserve execution context for exception handlers
- Don't reset context on exception, only on successful completion - This allows exception handlers to access execution_id naturally - Simplify crash handler since context is now available - Rely on Python's automatic contextvar cleanup when task completes - Each request runs in its own task, so no risk of context leakage This is more correct and follows the principle that context should be available throughout the entire request lifecycle, including error handling.
1 parent d03052f commit 565e22c

File tree

2 files changed

+25
-31
lines changed

2 files changed

+25
-31
lines changed

src/functions_framework/aio/__init__.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,23 +64,11 @@ async def _crash_handler(request, exc):
6464
tb_text = ''.join(tb_lines)
6565
error_msg = f"Exception on {request.url.path} [{request.method}]\n{tb_text}".rstrip()
6666

67-
# When execution ID logging is enabled, we need to extract execution_id from headers
68-
# because the decorator resets the context before exception handlers run
67+
# Context should still be available since we don't reset on exception
6968
if _enable_execution_id_logging():
70-
context = execution_id._extract_context_from_headers(request.headers)
71-
if context.execution_id:
72-
# Temporarily set context for logging
73-
token = execution_id.execution_context_var.set(context)
74-
try:
75-
# Output as JSON so LoggingHandlerAddExecutionId can process it
76-
log_entry = {"message": error_msg, "levelname": "ERROR"}
77-
logger.error(json.dumps(log_entry))
78-
finally:
79-
execution_id.execution_context_var.reset(token)
80-
else: # pragma: no cover
81-
# No execution ID in headers
82-
log_entry = {"message": error_msg, "levelname": "ERROR"}
83-
logger.error(json.dumps(log_entry))
69+
# Output as JSON so LoggingHandlerAddExecutionId can process it
70+
log_entry = {"message": error_msg, "levelname": "ERROR"}
71+
logger.error(json.dumps(log_entry))
8472
else:
8573
# Execution ID logging not enabled, log plain text
8674
logger.error(error_msg)

src/functions_framework/execution_id.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,11 @@ def wrapper(*args, **kwargs):
166166
_set_current_context(context)
167167

168168
with stderr_redirect, stdout_redirect:
169-
return view_function(*args, **kwargs)
169+
result = view_function(*args, **kwargs)
170+
171+
# Context cleanup happens automatically via Flask's request context
172+
# No need to manually clean up flask.g
173+
return result
170174

171175
return wrapper
172176

@@ -195,16 +199,17 @@ async def async_wrapper(request, *args, **kwargs):
195199
# Set context using contextvars
196200
token = execution_context_var.set(context)
197201

198-
try:
199-
with stderr_redirect, stdout_redirect:
200-
# Handle both sync and async functions
201-
if inspect.iscoroutinefunction(view_function):
202-
return await view_function(request, *args, **kwargs)
203-
else:
204-
return view_function(request, *args, **kwargs) # pragma: no cover
205-
finally:
206-
# Reset context
202+
with stderr_redirect, stdout_redirect:
203+
# Handle both sync and async functions
204+
if inspect.iscoroutinefunction(view_function):
205+
result = await view_function(request, *args, **kwargs)
206+
else:
207+
result = view_function(request, *args, **kwargs) # pragma: no cover
208+
209+
# Only reset context on successful completion
210+
# On exception, leave context available for exception handlers
207211
execution_context_var.reset(token)
212+
return result
208213

209214
@functools.wraps(view_function)
210215
def sync_wrapper(request, *args, **kwargs): # pragma: no cover
@@ -214,12 +219,13 @@ def sync_wrapper(request, *args, **kwargs): # pragma: no cover
214219
# Set context using contextvars
215220
token = execution_context_var.set(context)
216221

217-
try:
218-
with stderr_redirect, stdout_redirect:
219-
return view_function(request, *args, **kwargs)
220-
finally:
221-
# Reset context
222+
with stderr_redirect, stdout_redirect:
223+
result = view_function(request, *args, **kwargs)
224+
225+
# Only reset context on successful completion
226+
# On exception, leave context available for exception handlers
222227
execution_context_var.reset(token)
228+
return result
223229

224230
# Return appropriate wrapper based on whether the function is async
225231
if inspect.iscoroutinefunction(view_function):

0 commit comments

Comments
 (0)