lib/vcr/errors.rb
module VCR
# Namespace for VCR errors.
module Errors
# Base class for all VCR errors.
class Error < StandardError; end
# Error raised when VCR is turned off while a cassette is in use.
# @see VCR#turn_off!
# @see VCR#turned_off
class CassetteInUseError < Error; end
# Error raised when a VCR cassette is inserted while VCR is turned off.
# @see VCR#insert_cassette
# @see VCR#use_cassette
class TurnedOffError < Error; end
# Error raised when an cassette ERB template is rendered and a
# variable is missing.
# @see VCR#insert_cassette
# @see VCR#use_cassette
class MissingERBVariableError < Error; end
# Error raised when the version of one of the libraries that VCR hooks into
# is too low for VCR to support.
# @see VCR::Configuration#hook_into
class LibraryVersionTooLowError < Error; end
# Error raised when a request matcher is requested that is not registered.
class UnregisteredMatcherError < Error; end
# Error raised when a VCR 1.x cassette is used with VCR 2.
class InvalidCassetteFormatError < Error; end
# Error raised when an `around_http_request` hook is used improperly.
# @see VCR::Configuration#around_http_request
class AroundHTTPRequestHookError < Error; end
# Error raised when you attempt to use a VCR feature that is not
# supported on your ruby interpreter.
# @see VCR::Configuration#around_http_request
class NotSupportedError < Error; end
# Error raised when you ask VCR to decode a compressed response
# body but the content encoding isn't one of the known ones.
# @see VCR::Response#decompress
class UnknownContentEncodingError < Error; end
# Error raised when you eject a cassette before all previously
# recorded HTTP interactions are played back.
# @note Only applicable when :allow_episode_skipping is false.
# @see VCR::HTTPInteractionList#assert_no_unused_interactions!
class UnusedHTTPInteractionError < Error; end
# Error raised when you attempt to eject a cassette inserted by another
# thread.
class EjectLinkedCassetteError < Error; end
# Error raised when an HTTP request is made that VCR is unable to handle.
# @note VCR will raise this to force you to do something about the
# HTTP request. The idea is that you want to handle _every_ HTTP
# request in your test suite. The error message will give you
# suggestions for how to deal with the request.
class UnhandledHTTPRequestError < Error
# The HTTP request.
attr_reader :request
# Constructs the error.
#
# @param [VCR::Request] request the unhandled request.
def initialize(request)
@request = request
super construct_message
end
private
def documentation_version_slug
@documentation_version_slug ||= VCR.version.gsub(/\W/, '-')
end
def construct_message
["", "", "=" * 80,
"An HTTP request has been made that VCR does not know how to handle:",
"#{request_description}\n",
cassettes_description,
formatted_suggestions,
"=" * 80, "", ""].join("\n")
end
def current_cassettes
@cassettes ||= VCR.cassettes.to_a.reverse
end
def request_description
lines = []
lines << " #{request.method.to_s.upcase} #{request.uri}"
if match_request_on_headers?
lines << " Headers:\n#{formatted_headers}"
end
if match_request_on_body?
lines << " Body: #{request.body}"
end
lines.join("\n")
end
def match_request_on_headers?
current_matchers.include?(:headers)
end
def match_request_on_body?
current_matchers.include?(:body)
end
def current_matchers
if current_cassettes.size > 0
current_cassettes.inject([]) do |memo, cassette|
memo | cassette.match_requests_on
end
else
VCR.configuration.default_cassette_options[:match_requests_on]
end
end
def formatted_headers
request.headers.flat_map do |header, values|
values.map do |val|
" #{header}: #{val.inspect}"
end
end.join("\n")
end
def cassettes_description
if current_cassettes.size > 0
[cassettes_list << "\n",
"Under the current configuration VCR can not find a suitable HTTP interaction",
"to replay and is prevented from recording new requests. There are a few ways",
"you can deal with this:\n"].join("\n")
else
["There is currently no cassette in use. There are a few ways",
"you can configure VCR to handle this request:\n"].join("\n")
end
end
def cassettes_list
lines = []
lines << if current_cassettes.size == 1
"VCR is currently using the following cassette:"
else
"VCR are currently using the following cassettes:"
end
lines = current_cassettes.inject(lines) do |memo, cassette|
memo.concat([
" - #{cassette.file}",
" - :record => #{cassette.record_mode.inspect}",
" - :match_requests_on => #{cassette.match_requests_on.inspect}"
])
end
lines.join("\n")
end
def formatted_suggestions
formatted_points, formatted_foot_notes = [], []
suggestions.each_with_index do |suggestion, index|
bullet_point, foot_note = suggestion.first, suggestion.last
formatted_points << format_bullet_point(bullet_point, index)
formatted_foot_notes << format_foot_note(foot_note, index)
end
[
formatted_points.join("\n"),
formatted_foot_notes.join("\n")
].join("\n\n")
end
def format_bullet_point(lines, index)
lines.first.insert(0, " * ")
lines.last << " [#{index + 1}]."
lines.join("\n ")
end
def format_foot_note(url, index)
"[#{index + 1}] #{url % documentation_version_slug}"
end
# List of suggestions for how to configure VCR to handle the request.
ALL_SUGGESTIONS = {
:use_new_episodes => [
["You can use the :new_episodes record mode to allow VCR to",
"record this new request to the existing cassette"],
"https://benoittgt.github.io/vcr/?v=%s#/record_modes/new_episodes"
],
:delete_cassette_for_once => [
["The current record mode (:once) does not allow new requests to be recorded",
"to a previously recorded cassette. You can delete the cassette file and re-run",
"your tests to allow the cassette to be recorded with this request"],
"https://benoittgt.github.io/vcr/?v=%s#/record_modes/once"
],
:deal_with_none => [
["The current record mode (:none) does not allow requests to be recorded. You",
"can temporarily change the record mode to :once, delete the cassette file ",
"and re-run your tests to allow the cassette to be recorded with this request"],
"https://benoittgt.github.io/vcr/?v=%s#/record_modes/none"
],
:use_a_cassette => [
["If you want VCR to record this request and play it back during future test",
"runs, you should wrap your test (or this portion of your test) in a",
"`VCR.use_cassette` block"],
"https://benoittgt.github.io/vcr/?v=%s#/getting_started"
],
:allow_http_connections_when_no_cassette => [
["If you only want VCR to handle requests made while a cassette is in use,",
"configure `allow_http_connections_when_no_cassette = true`. VCR will",
"ignore this request since it is made when there is no cassette"],
"https://benoittgt.github.io/vcr/?v=%s#/configuration/allow_http_connections_when_no_cassette"
],
:ignore_request => [
["If you want VCR to ignore this request (and others like it), you can",
"set an `ignore_request` callback"],
"https://benoittgt.github.io/vcr/?v=%s#/configuration/ignore_request"
],
:allow_playback_repeats => [
["The cassette contains an HTTP interaction that matches this request,",
"but it has already been played back. If you wish to allow a single HTTP",
"interaction to be played back multiple times, set the `:allow_playback_repeats`",
"cassette option"],
"https://benoittgt.github.io/vcr/?v=%s#/request_matching/playback_repeats"
],
:match_requests_on => [
["The cassette contains %s not been",
"played back. If your request is non-deterministic, you may need to",
"change your :match_requests_on cassette option to be more lenient",
"or use a custom request matcher to allow it to match"],
"https://benoittgt.github.io/vcr/?v=%s#/request_matching"
],
:try_debug_logger => [
["If you're surprised VCR is raising this error",
"and want insight about how VCR attempted to handle the request,",
"you can use the debug_logger configuration option to log more details"],
"https://benoittgt.github.io/vcr/?v=%s#/configuration/debug_logging"
]
}
def suggestion_for(key)
bullet_point_lines, url = ALL_SUGGESTIONS[key]
bullet_point_lines = bullet_point_lines.map(&:dup)
url = url.dup
[bullet_point_lines, url]
end
def suggestions
return no_cassette_suggestions if current_cassettes.size == 0
[:try_debug_logger, :use_new_episodes, :ignore_request].tap do |suggestions|
suggestions.push(*record_mode_suggestion)
suggestions << :allow_playback_repeats if has_used_interaction_matching?
suggestions.map! { |k| suggestion_for(k) }
suggestions.push(*match_requests_on_suggestion)
end
end
def no_cassette_suggestions
[:try_debug_logger, :use_a_cassette, :allow_http_connections_when_no_cassette, :ignore_request].map do |key|
suggestion_for(key)
end
end
def record_mode_suggestion
record_modes = current_cassettes.map(&:record_mode)
if record_modes.all?{|r| r == :none }
[:deal_with_none]
elsif record_modes.all?{|r| r == :once }
[:delete_cassette_for_once]
else
[]
end
end
def has_used_interaction_matching?
current_cassettes.any?{|c| c.http_interactions.has_used_interaction_matching?(request) }
end
def match_requests_on_suggestion
num_remaining_interactions = current_cassettes.inject(0) { |sum, c|
sum + c.http_interactions.remaining_unused_interaction_count
}
return [] if num_remaining_interactions.zero?
interaction_description = if num_remaining_interactions == 1
"1 HTTP interaction that has"
else
"#{num_remaining_interactions} HTTP interactions that have"
end
description_lines, link = suggestion_for(:match_requests_on)
description_lines[0] = description_lines[0] % interaction_description
[[description_lines, link]]
end
end
end
end