Fix timeout watchdog firing while disarmed by anconina · Pull Request #8451 · mitmproxy/mitmproxy · GitHub
Skip to content

Fix timeout watchdog firing while disarmed - #8451

Open
anconina wants to merge 4 commits into
mitmproxy:mainfrom
anconina:fm/mitmproxy-timeout-watchdog
Open

anconina wants to merge 4 commits into
mitmproxy:mainfrom
anconina:fm/mitmproxy-timeout-watchdog

Conversation

@anconina

@anconina anconina commented Sep 28, 2026 •

Copy link
Copy Markdown

Description

TimeoutWatchdog.watch() waits for the watchdog to be enabled and then sleeps until the inactivity deadline. If a blocking addon hook disarms the watchdog during that sleep, the existing code can still invoke the timeout callback after waking because it only checks the activity deadline. On a live connection, that closes the client while the hook is still running.

I reproduced this with a standalone asyncio script that starts a local HTTP server and a regular DumpMaster proxy with tcp_timeout=1. An async request hook sleeps for two seconds; the client sends a GET through the proxy immediately after the watchdog enters its sleep.

  • Before this change, mitmproxy logged Closing connection due to inactivity at 1.00 seconds and the client received EOF (b'').
  • With this change, the hook completed at 2.01 seconds and the client received HTTP/1.1 200 OK with the expected body.

The watchdog now sleeps first and then waits for the event before checking the activity deadline (suggested in review). If it was disarmed during the sleep, it blocks until it's re-armed. Re-arming refreshes the activity timestamp, so the next check can't fire on a stale deadline and the loop doesn't spin. The final nested disarm() exit continues to register activity and re-enable the watchdog, and the configured timeout remains unchanged.

The regression tests cover:

  • disarming during the watchdog sleep;
  • nested disarms;
  • re-arming without a busy loop; and
  • activity moving the timeout deadline.

On unfixed main, the two disarm-depth cases failed and the activity-deadline case passed. With this change, all three cases pass.

Validation:

  • uv run pytest test/mitmproxy/proxy: 467 passed, 4 skipped.
  • uv run tox -e lint: passed.

Checklist

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

@mhils mhils left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR!

Comment thread mitmproxy/proxy/server.py Outdated
Comment on lines 70 to 71

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would just swapping those two statements also work? 🤔

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it does. Thanks, that's much simpler! Sleeping first and then waiting for can_timeout handles every case the new tests cover: a disarm during the sleep (nested or not), re-arming without a busy loop, and activity moving the deadline. That works because disarm() refreshes last_activity when it re-arms, so the deadline check right after wait() can't fire on a stale timestamp.

I've switched the fix to the swap in 657fba1 and kept the tests. On main, the two disarm tests fail, and with the swap all three pass. With a standalone DumpMaster and tcp_timeout=1, a 2-second async client_connected hook gets Closing connection due to inactivity at 1.00s on main. With the swap, nothing is closed and the request completes. I also merged main into the branch, which picks up the Debian 12 builder that fixes the failing Linux build jobs.

This branch has not been deployed

No deployments
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.

2 participants