Skip to content

before_record_request called when replaying #1042

Description

@ciastek

What happened?

From the Custom Response Filtering docs:

You can also do response filtering with the before_record_response configuration option. (...) you can mutate the response, or return None to avoid recording the request and response altogether. For example to hide sensitive data from the response body: (...)

I assumed, that before_record_response would only filter responses when recording a cassette. However, to my surprise (and others', see: #268 and #325), it also filters them while replaying. Here is an example:

#!/usr/bin/env -S uv run --script
#
# /// script
# requires-python = ">=3.10"
# dependencies = ["vcrpy >=2.1.0,<=8.3.0,!=4.0.0,!=4.0.1"]
# ///

import json, os, urllib.request, vcr, yaml

CASSETTE_PATH = 'cassette.yaml'
PER_PAGE = 3
RELEASES_URI = f'https://api.github.com/repos/kevin1024/vcrpy/releases?per_page={PER_PAGE}'

def remove_last_release(response):
    body = json.loads(response['body']['string'].decode())
    body.pop()
    response['body']['string'] = json.dumps(body).encode()
    return response

my_vcr = vcr.VCR(before_record_response=remove_last_release)

# When recording, the original response is returned
assert not os.path.exists(CASSETTE_PATH)
with my_vcr.use_cassette(CASSETTE_PATH):
    response = urllib.request.urlopen(RELEASES_URI).read()
    body = json.loads(response)
    assert len(body) == PER_PAGE

# Recorded response is modified by before_record_response
assert os.path.exists(CASSETTE_PATH)
with open(CASSETTE_PATH) as cassette_file:
    cassette = yaml.safe_load(cassette_file)
    body = json.loads(cassette['interactions'][0]['response']['body']['string'])
    assert len(body) == PER_PAGE - 1

# Replayed response is modifed again by before_record_response
assert os.path.exists(CASSETTE_PATH)
with my_vcr.use_cassette(CASSETTE_PATH):
    response = urllib.request.urlopen(RELEASES_URI).read()
    body = json.loads(response)
    assert len(body) == PER_PAGE - 2

os.remove(CASSETTE_PATH)

Should it happen?

A comment on the pull request that introduced before_record_response states:

This is mainly for the purpose of scrubbing responses

The repository owner confirmed this purpose in another comment:

(...) the intention of before_record_response is to give a hook to alter the data that gets saved

I haven't found any tests confirming that running before_record_response during replay is expected behavior.

These premises, and primarily the name before_record_response itself, lead me to believe that filtering on replay is a bug.

Why it happens

The before_record_response configuration was added in the Add before_record_response to Cassette and VCR commit. It was included as a rather unrelated part of the large Fix cassette context decorator nesting issues pull request and was released in version 1.1.0.

The call to the before_record_response filter (or filters, since version 1.1.1) was placed in the Cassette.append method. This method is called by stubs while recording and by the Cassette._load method when loading an existing cassette. The latter is the reason responses are filtered a second time when replaying.

What now?

The existing behavior should be acknowledged, documented, and tested.

Moving forward, the before_record_response call should be modified to execute only when recording. Since the current behavior—even if unintended—has been in place for 12 years, this change would require a major version bump.

But what about AI?

The initial version of the example was created by Gemini 3.5 Flash-Lite.
The investigation was conducted by pi using unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions