#8363·mitmproxy

fix(server-replay): apply the extra-request policy after replay exhaustion

Author: coygeekCreated Aug 10, 2026Updated Aug 10, 2026

Summary

Target repository: mitmproxy/mitmproxy.

When server replay is configured without server_replay_reuse, consuming the final replayable response empties the replay map. Every later request then bypasses server_replay_extra, including an identical request that no longer has a replayable response, and is allowed to reach the upstream server.

This contradicts the option's documented contract: server_replay_extra controls requests during replay for which no replayable response was found. It also makes numeric policies such as 404 stop applying silently after the last saved response is consumed.

Steps to reproduce

The following uses only a loopback origin and mitmproxy's explicit proxy mode.

  1. Save this as origin.py:

    from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
    from pathlib import Path
    
    
    class Handler(BaseHTTPRequestHandler):
        def do_GET(self):
            with Path("origin.log").open("a", encoding="utf-8") as log:
                log.write(self.path + "\n")
            body = b"origin response\n"
            self.send_response(200)
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
    
        def log_message(self, format, *args):
            pass
    
    
    ThreadingHTTPServer(("127.0.0.1", 18080), Handler).serve_forever()
    
  2. Start the origin in one terminal:

    : > origin.log
    python3 origin.py
    
  3. In a second terminal, record one request and stop the recording proxy:

    mitmdump \
      --listen-host 127.0.0.1 \
      --listen-port 18081 \
      --save-stream-file one.mitm \
      --flow-detail 0 &
    record_pid=$!
    sleep 1
    
    curl --noproxy '' --proxy http://127.0.0.1:18081 \
      http://127.0.0.1:18080/recorded
    
    kill "$record_pid"
    wait "$record_pid"
    wc -l origin.log
    

    The origin count is 1.

  4. Start server replay with a 404 policy, lazy upstream connections, and the default non-reusable replay behavior:

    mitmdump \
      --listen-host 127.0.0.1 \
      --listen-port 18082 \
      --server-replay one.mitm \
      --server-replay-extra 404 \
      --set connection_strategy=lazy \
      --no-server-replay-refresh \
      --flow-detail 0 &
    replay_pid=$!
    sleep 1
    
  5. Send the recorded request once and inspect the origin count:

    curl --noproxy '' --output /dev/null --write-out '%{http_code}\n' \
      --proxy http://127.0.0.1:18082 \
      http://127.0.0.1:18080/recorded
    wc -l origin.log
    

    The response is the replayed 200, and the origin count remains 1.

  6. Send the same request a second time:

    curl --noproxy '' --output /dev/null --write-out '%{http_code}\n' \
      --proxy http://127.0.0.1:18082 \
      http://127.0.0.1:18080/recorded
    wc -l origin.log
    
    kill "$replay_pid"
    wait "$replay_pid"
    

Expected behavior

The second replay request no longer has a replayable response, so server_replay_extra=404 should return an empty 404 response. The origin request count should remain 1.

More generally, the selected extra-request policy should remain active while server replay is configured, even when the non-reusable replay map has become empty.

A regression test can load one response, consume it with reuse disabled, issue a second request, and require the configured 404 response without contacting the origin. Equivalent checks should retain the established kill and forward semantics.

Actual behavior

The second request returns the origin's 200, and the origin request count increases from 1 to 2. The configured 404 policy is not applied after the final replay flow is consumed.

The minimized end-to-end result on the latest release was:

{
  "record_status": 200,
  "origin_requests_before_replay": 1,
  "first_replay_status": 200,
  "origin_requests_after_first": 1,
  "second_request_status": 200,
  "origin_requests_after_second": 2
}

Affected area

  • Server replay with server_replay_reuse=false, which is the default.
  • server_replay_extra=kill and numeric extra-response policies after the final saved response is consumed.
  • mitmproxy/addons/serverplayback.py, specifically the request-policy guard and replay-map removal path.
  • test/mitmproxy/addons/test_serverplayback.py, whose current extra-policy tests cover an unmatched request only while another replay flow remains in the map.

Runtime or environment

Latest release reproduction:

Source revision: 6c09d56e4c29a92f5ad01b03199977584b8ea14f (v12.2.3)
Mitmproxy: 12.2.3
Python:    3.14.5
OpenSSL:   OpenSSL 3.5.5 27 Jan 2026
Platform:  macOS-26.5.2-arm64-arm-64bit-Mach-O

The same minimized addon-level reproduction also fails on current main revision 88462f4ab376f1c09e5b467411b2b8751b5d12d0 (13.0.0.dev0).

Evidence

On both revisions, ServerPlayback.request wraps replay lookup and every server_replay_extra branch in if self.flowmap:. With reuse disabled, next_flow removes the final saved response and deletes its hash key. Once that was the last key, self.flowmap is empty, so the next request skips both replay lookup and the configured extra-request policy.

The corresponding minimized addon-level observation is:

{
  "first_status": 200,
  "replay_count_after_first": 0,
  "second_status": null,
  "second_error": null
}

The existing option description says that server_replay_extra defines the behavior for extra requests during replay for which no replayable response was found. The second request meets that condition, but the implementation no longer evaluates the option because the map is empty.

Impact

One-shot server-replay fixtures stop enforcing their selected unmatched-request behavior as soon as all saved responses have been consumed. Repeated or later requests can unexpectedly reach real upstream services instead of receiving the configured synthetic response or being killed.

This is especially surprising in offline tests, deterministic mocks, and other workflows that intentionally select a non-forwarding extra-request policy. server_replay_reuse=true avoids map exhaustion, but changes the requested one-shot replay semantics and is not equivalent for recordings containing ordered duplicate responses.

Additional context

This report is limited to the non-security behavioral contract and does not claim a vulnerability.

Semantic duplicate searches covered open and closed Mitmproxy issues and pull requests for server_replay_extra, exhausted or empty replay maps, repeated/second requests, 404 behavior, kill behavior, forwarding, and replay reuse:

  • #581 and #5024 establish the intended ability to prevent HTTP requests from reaching the destination during server replay, but neither covers policy bypass after the replay map becomes empty.
  • #3489 and merged PR #6465 introduced numeric extra-response policies, but their regression tests exercise an unmatched host while a different replay flow remains available.
  • #7890 concerns eager TCP/TLS connection establishment before the extra policy runs; connection_strategy=lazy resolves that separate behavior and was enabled in this reproduction.
  • Open #6113 and #6114 concern addon/script ordering with the kill option, not exhaustion of the replay map.

No matching open issue or pull request was found.

  • Reproduced on the latest mitmproxy release.