--- title: "Debugging vcr failures" output: html_vignette vignette: > %\VignetteIndexEntry{Debugging vcr failures} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} editor: markdown: wrap: sentence --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) # We want to re-record these cassettes every time vcr::vcr_configure(dir = tempdir()) ``` This vignette helps you debug the vcr error that you're most likely to encounter: "Failed to find matching request in active cassette.". If you're lucky, the request has genuinely changed, and you can make this problem go away by deleting the previous cassette so vcr can re-record it 🙂. Otherwise, you'll need to do some debugging. First we'll talk about logging, and how you can use it to better understand the process by which vcr matches the requests you make to the request-response pairs saved in a cassette. Then we'll work through a few of the most common problems and discuss specific solutions. In this vignette, we'll use httr2 for generating the requests. The same principles apply if you're working with crul or httr, just the code for making requests will look different. We'll also start up a local httpbin server using {webfakes}: this will let us make some practice requests to a local server. ```{r setup} library(vcr) library(httr2) httpbin <- webfakes::local_app_process(webfakes::httpbin_app()) ``` ```{r} #| include: false # Override default logging output so we don't need to explain the difference # between logging in vignettes and tests, and the reader can just copy and # paste code local_vcr_configure_log <- function(..., frame = parent.frame()) { vcr::local_vcr_configure_log(..., file = stdout(), frame = frame) } # Make a dummy test_that() that basically works the same way as local() # Using testthat::test_that() adds a bit too much noise to the examples. test_that <- function(desc, code) { eval(substitute(code), new.env(parent = parent.frame())) } ``` ## Logging basics The best tool to understand why your request isn't matching is logging. You can turn logging on for a single test with `local_vcr_configure_log()`. Let's turn on logging and see what happens the first time you make a request: ```{r} test_that("example test", { local_vcr_configure_log() local_cassette("debugging-1") req <- request(httpbin$url("/get")) resp <- req_perform(req) }) ``` 1. A new cassette is inserted, and is placed in recording mode. 2. The request is handled, and because we're in recording mode, it's recorded to disk. 3. The cassette is ejected. ### Second and subsequent requests If we run that code another time, the flow is slightly different: ```{r} test_that("example test", { local_vcr_configure_log() local_cassette("debugging-1") req <- request(httpbin$url("/get")) resp <- req_perform(req) }) ``` 1. The previously created cassette is inserted, loading one interaction from disk. This time it's in replaying mode. 2. The request is handled, but this time we're in replay mode, so vcr looks for it in the cassette. vcr finds it so it can replay the recorded response. 3. The cassette is ejected. ### A match failure We can now deliberately make a different request to see what happens. We'll leave everything the same but change the method to `POST`: ```{r} #| error: true test_that("example test", { local_vcr_configure_log() local_cassette("debugging-1") req <- request(httpbin$url("/get")) |> req_method("POST") resp <- req_perform(req) }) ``` This time you can see vcr didn't find any matches because there was only one request, and that request (`matching`) had a different method to the request we were trying to match. What happens if we change the URL? ```{r} #| error: true test_that("example test", { local_vcr_configure_log() local_cassette("debugging-1") req <- request(httpbin$url("/set")) resp <- req_perform(req) }) ``` Again, you can see why the request didn't match. vcr compares the parsed urls using {waldo}, hopefully making it easy to see exactly what's different. ### Multiple requests Before we move on to specific scenarios, let's look at a case where we record multiple requests: ```{r} test_that("example test", { local_vcr_configure_log() local_cassette("debugging-2") resp1 <- req_perform(request(httpbin$url("/get?x=1"))) resp2 <- req_perform(request(httpbin$url("/get?x=2"))) resp3 <- req_perform(request(httpbin$url("/get?x=3"))) }) ``` Now if we deliberately change the url, you can see that it looks at all three previously saved requests before giving up. ```{r} #| error: true test_that("example test", { local_vcr_configure_log() local_cassette("debugging-2") resp4 <- req_perform(request(httpbin$url("/get?x=4"))) }) ``` You'll notice that this is substantially more complicated to understand, and you can imagine it only gets harder the more requests you have. That's a good reason to keep your tests small and simple, and limited to only a couple of requests. ## Common problems and their solutions Now that you understand how to use logging to see exactly what vcr is doing, we can talk about some solutions to common problems. ### Re-used cassette name You need to make sure that every test has its own unique cassette name, otherwise you won't find the responses that you're expecting. This is generally a fairly easy problem to diagnose if you're aware of it: if you re-record the cassette to fix the one test, a different test will break. (Technically, it is ok to share the same cassette name if you want to use the same request for different tests. That's one of the reasons that vcr can't automatically spot this problem for you.) ### Non-deterministic requests Sometimes it's not possible to make the same request again and again. For example, in some cases an API will require a "nonce", a random string used to ensure that every request is unique: ```{r} #| error: true # Imagine this function is in R/ make_request <- function() { nonce <- paste0(sample(letters, 10, replace = TRUE), collapse = "") req <- request(httpbin$url("/get")) req <- req_url_query(req, nonce = nonce) req_perform(req) } # And this code is in tests/testthat/ test_that("example test", { local_cassette("nondeterministic") resp <- make_request() }) test_that("example test", { local_vcr_configure_log() local_cassette("nondeterministic") resp <- make_request() }) ``` You can solve this problem by trimming that component from the query string using the `filter_query_parameters` option. This will remove the specific query parameter names from the uri being matched. ```{r} test_that("example test", { local_vcr_configure_log() local_vcr_configure(filter_query_parameters = "nonce") local_cassette("nondeterministic") resp <- make_request() }) ``` In the less common case where you're matching on headers, you can use the `filter_request_headers` configuration to achieve the same effect.