Skip to content

Conversation

@bokelley
Copy link
Collaborator

Problem

A2A server was using threading.local() to store request context (auth token and headers), which doesn't work properly with async/await code. Thread-local storage is thread-specific, but async functions can execute on different threads, causing context variables to become inaccessible in async handlers.

This manifested as 'No tenant context set' errors in list_authorized_properties and other A2A endpoints because:

  1. Middleware set _request_context.request_headers in one thread
  2. Async handler tried to read it via getattr(_request_context, 'request_headers', {})
  3. Async handler was running in different thread → got empty dict
  4. Tenant detection failed → get_current_tenant() raised RuntimeError

Solution

Replace threading.local() with contextvars.ContextVar, which properly handles async contexts:

Before:
```python
_request_context = threading.local()
_request_context.auth_token = token
_request_context.request_headers = dict(request.headers)
token = getattr(_request_context, 'auth_token', None)
```

After:
```python
_request_auth_token: contextvars.ContextVar[str | None] = contextvars.ContextVar('request_auth_token', default=None)
_request_headers: contextvars.ContextVar[dict | None] = contextvars.ContextVar('request_headers', default=None)
_request_auth_token.set(token)
_request_headers.set(dict(request.headers))
token = _request_auth_token.get()
```

Changes

Production Code

  • src/a2a_server/adcp_a2a_server.py: Replaced threading.local() with two ContextVars
    • Updated all 5 access points to use .get() and .set() methods
    • Updated middleware cleanup to use .set(None) instead of delattr()
    • Added explanatory comment about ContextVars working with async code

Test Code

  • tests/integration/test_a2a_response_message_fields.py: Updated 1 test fixture
  • tests/integration_v2/test_a2a_skill_invocation.py: Updated 20 tests
  • All tests now use adcp_a2a_server._request_headers.set() instead of mocking

Schemas

  • Updated cached AdCP schemas (create/update media buy requests) to latest versions

Testing

  • ✅ All 20 A2A integration tests pass
  • ✅ No breaking changes - ContextVars are drop-in replacement for threading.local()
  • ✅ All pre-commit hooks pass
  • ✅ Fixes 'No tenant context set' error in list_authorized_properties A2A calls

References

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

bokelley and others added 2 commits October 27, 2025 06:22
…quest context

## Problem
A2A server was using threading.local() to store request context (auth token and headers),
which doesn't work properly with async/await code. Thread-local storage is thread-specific,
but async functions can execute on different threads, causing context variables to become
inaccessible in async handlers.

This manifested as 'No tenant context set' errors in list_authorized_properties and other
A2A endpoints because:
1. Middleware set _request_context.request_headers in one thread
2. Async handler tried to read it via getattr(_request_context, 'request_headers', {})
3. Async handler was running in different thread → got empty dict
4. Tenant detection failed → get_current_tenant() raised RuntimeError

## Solution
Replace threading.local() with contextvars.ContextVar, which properly handles async contexts:

**Before:**
```python
_request_context = threading.local()
_request_context.auth_token = token
_request_context.request_headers = dict(request.headers)
token = getattr(_request_context, 'auth_token', None)
```

**After:**
```python
_request_auth_token: contextvars.ContextVar[str | None] = contextvars.ContextVar('request_auth_token', default=None)
_request_headers: contextvars.ContextVar[dict | None] = contextvars.ContextVar('request_headers', default=None)
_request_auth_token.set(token)
_request_headers.set(dict(request.headers))
token = _request_auth_token.get()
```

## Changes
- Replaced threading.local() with two ContextVars: _request_auth_token and _request_headers
- Updated all 5 access points to use .get() and .set() methods instead of getattr/setattr
- Updated middleware cleanup to use .set(None) instead of delattr()
- Added explanatory comment about ContextVars working with async code
- Updated cached AdCP schemas (create/update media buy requests)

## Testing
- Verified with tests/integration/test_a2a*.py (20 passed)
- No breaking changes - ContextVars are drop-in replacement for threading.local()
- Fixes 'No tenant context set' error in list_authorized_properties A2A calls

## References
- Python ContextVars docs: https://docs.python.org/3/library/contextvars.html
- Issue: 'Error loading properties: A2A agent returned error: Unable to retrieve authorized properties'

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Updated test fixtures to use adcp_a2a_server._request_headers.set() instead of
mocking the old _request_context.request_headers pattern.

Files fixed:
- tests/integration/test_a2a_response_message_fields.py (1 occurrence)
- tests/integration_v2/test_a2a_skill_invocation.py (20 occurrences)

All tests now compatible with the ContextVar-based request context.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@bokelley bokelley merged commit 719dabb into main Oct 27, 2025
9 checks passed
danf-newton pushed a commit to Newton-Research-Inc/salesagent that referenced this pull request Nov 24, 2025
…quest context (prebid#637)

* fix: Replace threading.local() with contextvars for async-safe A2A request context

## Problem
A2A server was using threading.local() to store request context (auth token and headers),
which doesn't work properly with async/await code. Thread-local storage is thread-specific,
but async functions can execute on different threads, causing context variables to become
inaccessible in async handlers.

This manifested as 'No tenant context set' errors in list_authorized_properties and other
A2A endpoints because:
1. Middleware set _request_context.request_headers in one thread
2. Async handler tried to read it via getattr(_request_context, 'request_headers', {})
3. Async handler was running in different thread → got empty dict
4. Tenant detection failed → get_current_tenant() raised RuntimeError

## Solution
Replace threading.local() with contextvars.ContextVar, which properly handles async contexts:

**Before:**
```python
_request_context = threading.local()
_request_context.auth_token = token
_request_context.request_headers = dict(request.headers)
token = getattr(_request_context, 'auth_token', None)
```

**After:**
```python
_request_auth_token: contextvars.ContextVar[str | None] = contextvars.ContextVar('request_auth_token', default=None)
_request_headers: contextvars.ContextVar[dict | None] = contextvars.ContextVar('request_headers', default=None)
_request_auth_token.set(token)
_request_headers.set(dict(request.headers))
token = _request_auth_token.get()
```

## Changes
- Replaced threading.local() with two ContextVars: _request_auth_token and _request_headers
- Updated all 5 access points to use .get() and .set() methods instead of getattr/setattr
- Updated middleware cleanup to use .set(None) instead of delattr()
- Added explanatory comment about ContextVars working with async code
- Updated cached AdCP schemas (create/update media buy requests)

## Testing
- Verified with tests/integration/test_a2a*.py (20 passed)
- No breaking changes - ContextVars are drop-in replacement for threading.local()
- Fixes 'No tenant context set' error in list_authorized_properties A2A calls

## References
- Python ContextVars docs: https://docs.python.org/3/library/contextvars.html
- Issue: 'Error loading properties: A2A agent returned error: Unable to retrieve authorized properties'

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Update tests to use ContextVars instead of threading.local()

Updated test fixtures to use adcp_a2a_server._request_headers.set() instead of
mocking the old _request_context.request_headers pattern.

Files fixed:
- tests/integration/test_a2a_response_message_fields.py (1 occurrence)
- tests/integration_v2/test_a2a_skill_invocation.py (20 occurrences)

All tests now compatible with the ContextVar-based request context.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant