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.
What happened?
From the Custom Response Filtering docs:
I assumed, that
before_record_responsewould 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:Should it happen?
A comment on the pull request that introduced
before_record_responsestates:The repository owner confirmed this purpose in another comment:
I haven't found any tests confirming that running
before_record_responseduring replay is expected behavior.These premises, and primarily the name
before_record_responseitself, lead me to believe that filtering on replay is a bug.Why it happens
The
before_record_responseconfiguration 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_responsefilter (or filters, since version 1.1.1) was placed in theCassette.appendmethod. This method is called by stubs while recording and by theCassette._loadmethod 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_responsecall 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.