Skip to content

Add async flow.request.stream functions support - #8367

Open
hdk5 wants to merge 1 commit into
mitmproxy:mainfrom
hdk5:stream-async
Open

Add async flow.request.stream functions support#8367
hdk5 wants to merge 1 commit into
mitmproxy:mainfrom
hdk5:stream-async

Conversation

@hdk5

@hdk5 hdk5 commented Aug 11, 2026

Copy link
Copy Markdown

Description

Message.stream transformers currently run synchronously while HttpStream processes request or response body data. If a transformer performs blocking work, it blocks mitmproxy’s main asyncio event loop. This pauses not only the affected HTTP stream, but also unrelated connections handled by that loop.

This PR adds explicit, opt-in concurrency for blocking stream transformers while preserving the existing synchronous fast path.

Existing transformers continue to work unchanged:

def transform(chunk: bytes) -> bytes:
    return chunk.upper()

flow.request.stream = transform

Blocking synchronous transformers can now opt into worker-thread execution:

from mitmproxy.script import run_in_thread

@run_in_thread
def transform(chunk: bytes) -> bytes:
    return blocking_operation(chunk)

flow.request.stream = transform

Native asynchronous transformers are also supported:

async def transform(chunk: bytes) -> bytes:
    return await asynchronous_operation(chunk)

flow.response.stream = transform

This works for both flow.request.stream and flow.response.stream.

How asynchronous processing is integrated

The proxy layer command protocol now has a generic Await command and corresponding AwaitCompleted event. When an HTTP stream encounters an awaitable, it yields this blocking command and pauses only the layer that issued it.

The connection handler schedules the awaitable as an asyncio task instead of awaiting it inline. This allows the current HttpStream to remain paused while unrelated streams and connections continue to be processed. Results and exceptions are returned through the normal command-completion mechanism.

run_in_thread

mitmproxy.script.run_in_thread converts supported synchronous callables into regular asynchronous callables:

  • A normal synchronous function becomes an async function whose invocation uses asyncio.to_thread.
  • A synchronous generator function becomes an async generator. Each next() call runs through asyncio.to_thread, preserving incremental streaming and backpressure instead of consuming the complete generator in a worker thread.
  • A normal function that manually returns a generator is intentionally not treated as a generator function. Supporting that would require inspecting arbitrary return values. Callers that need threaded incremental iteration should use an actual generator function.
Development history

The stream-async-wip branch preserves the more detailed development history. It also contains several earlier designs that were rejected.

Checklist

  • I have updated tests where applicable.
  • I have added an entry to the CHANGELOG.

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