diff --git a/lib/ruby_llm/connection.rb b/lib/ruby_llm/connection.rb index 5c9245988..71f4ca16a 100644 --- a/lib/ruby_llm/connection.rb +++ b/lib/ruby_llm/connection.rb @@ -77,7 +77,8 @@ def setup_retry(faraday) interval_randomness: @config.retry_interval_randomness, backoff_factor: @config.retry_backoff_factor, exceptions: retry_exceptions, - retry_statuses: [429, 500, 502, 503, 504, 529] + retry_statuses: [429, 500, 502, 503, 504, 529], + methods: %i[delete get head options patch post put] } end diff --git a/spec/fixtures/vcr_cassettes/chat_error_handling_with_vertexai_gemini-2_5-flash_faraday_version_1_retries_the_request.yml b/spec/fixtures/vcr_cassettes/chat_error_handling_with_vertexai_gemini-2_5-flash_faraday_version_1_retries_the_request.yml new file mode 100644 index 000000000..bd562b87e --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_error_handling_with_vertexai_gemini-2_5-flash_faraday_version_1_retries_the_request.yml @@ -0,0 +1,47 @@ +--- +http_interactions: +- request: + method: post + uri: https://www.googleapis.com/oauth2/v4/token + body: + encoding: ASCII-8BIT + string: grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJsdWlnaS1zZXJ2ZXJsZXNzQGFjaG1lZC1rYy5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImF1ZCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92NC90b2tlbiIsImV4cCI6MTc2NDYwNjE4MywiaWF0IjoxNzY0NjA2MDYzLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvY2xvdWQtcGxhdGZvcm0gaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9nZW5lcmF0aXZlLWxhbmd1YWdlLnJldHJpZXZlciJ9.FEAjh0sb7Dbyk-d8cEC1BBnITXID3zRpiMMgz2u7Fk3Gd_E0PAPbgPGGeRqQ6bWnxx2FvIQ1CePPbyDKaCue6EKcNshraXeSOpC3LpZ9m5chLQvfs2sc_JlI0_KzySkdM9bS09q3ceXYJkgWjFV7Fl_wodC5x8CSxnZ2HiFLoc_1pVug0TlNsqoKaiHrJ8T1K4fp72jjcGWB8McfLQkMPHOlFt8JaiB5cYDf_zM4swPcQZ76-qA64zJAx8WK3c-zE4ZxqRtWvIFx_4dYt4_VHalQ0BRTNZ0KeKT45gzbidD9d1TCpaXRe1fvR6GMVmKPBlaq_yrIhi3whM6lYMwgpQ + headers: + User-Agent: + - Faraday v2.14.0 + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Vary: + - Origin + - Referer + - X-Origin + Date: + - Mon, 01 Dec 2025 16:22:03 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: '{"access_token":"","expires_in":3599,"token_type":"Bearer"}' + recorded_at: Mon, 01 Dec 2025 16:22:03 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_error_handling_with_vertexai_gemini-2_5-flash_faraday_version_2_retries_the_request.yml b/spec/fixtures/vcr_cassettes/chat_error_handling_with_vertexai_gemini-2_5-flash_faraday_version_2_retries_the_request.yml new file mode 100644 index 000000000..9417161a5 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_error_handling_with_vertexai_gemini-2_5-flash_faraday_version_2_retries_the_request.yml @@ -0,0 +1,47 @@ +--- +http_interactions: +- request: + method: post + uri: https://www.googleapis.com/oauth2/v4/token + body: + encoding: ASCII-8BIT + string: grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJsdWlnaS1zZXJ2ZXJsZXNzQGFjaG1lZC1rYy5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImF1ZCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92NC90b2tlbiIsImV4cCI6MTc2NDYwNjE4MywiaWF0IjoxNzY0NjA2MDYzLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvY2xvdWQtcGxhdGZvcm0gaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9nZW5lcmF0aXZlLWxhbmd1YWdlLnJldHJpZXZlciJ9.FEAjh0sb7Dbyk-d8cEC1BBnITXID3zRpiMMgz2u7Fk3Gd_E0PAPbgPGGeRqQ6bWnxx2FvIQ1CePPbyDKaCue6EKcNshraXeSOpC3LpZ9m5chLQvfs2sc_JlI0_KzySkdM9bS09q3ceXYJkgWjFV7Fl_wodC5x8CSxnZ2HiFLoc_1pVug0TlNsqoKaiHrJ8T1K4fp72jjcGWB8McfLQkMPHOlFt8JaiB5cYDf_zM4swPcQZ76-qA64zJAx8WK3c-zE4ZxqRtWvIFx_4dYt4_VHalQ0BRTNZ0KeKT45gzbidD9d1TCpaXRe1fvR6GMVmKPBlaq_yrIhi3whM6lYMwgpQ + headers: + User-Agent: + - Faraday v2.14.0 + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Vary: + - Origin + - Referer + - X-Origin + Date: + - Mon, 01 Dec 2025 16:22:04 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: '{"access_token":"","expires_in":3599,"token_type":"Bearer"}' + recorded_at: Mon, 01 Dec 2025 16:22:04 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/ruby_llm/chat_error_spec.rb b/spec/ruby_llm/chat_error_spec.rb index a5dfd8a74..ee0d6a2aa 100644 --- a/spec/ruby_llm/chat_error_spec.rb +++ b/spec/ruby_llm/chat_error_spec.rb @@ -29,6 +29,12 @@ RSpec.describe RubyLLM::Chat do include_context 'with configured RubyLLM' + before do + RubyLLM.configure do |config| + config.max_retries = 0 + end + end + describe 'error handling' do CHAT_MODELS.each do |model_info| model = model_info[:model] diff --git a/spec/ruby_llm/chat_streaming_spec.rb b/spec/ruby_llm/chat_streaming_spec.rb index fc6ee8d9a..bfbbb7271 100644 --- a/spec/ruby_llm/chat_streaming_spec.rb +++ b/spec/ruby_llm/chat_streaming_spec.rb @@ -88,6 +88,18 @@ end end.to raise_error(expected_error_for(provider)) end + + it 'retries the request' do + stub_error_response(provider, :chunk) + + expect do + chat.ask('Count from 1 to 3') do |chunk| + chunks << chunk + end + end.to raise_error(expected_error_for(provider)) + + expect(WebMock).to have_requested(:post, expected_url_for(provider)).times(3) + end end describe 'Faraday version 2' do # rubocop:disable RSpec/NestedGroups @@ -124,6 +136,18 @@ end end.to raise_error(expected_error_for(provider)) end + + it 'retries the request' do + stub_error_response(provider, :chunk) + + expect do + chat.ask('Count from 1 to 3') do |chunk| + chunks << chunk + end + end.to raise_error(expected_error_for(provider)) + + expect(WebMock).to have_requested(:post, expected_url_for(provider)).times(3) + end end end end diff --git a/spec/support/rubyllm_configuration.rb b/spec/support/rubyllm_configuration.rb index 25a2a5b90..bddc6f14b 100644 --- a/spec/support/rubyllm_configuration.rb +++ b/spec/support/rubyllm_configuration.rb @@ -28,10 +28,10 @@ config.vertexai_location = ENV.fetch('GOOGLE_CLOUD_LOCATION', 'us-central1') config.request_timeout = 240 - config.max_retries = 10 - config.retry_interval = 1 - config.retry_backoff_factor = 3 - config.retry_interval_randomness = 0.5 + config.max_retries = 2 + config.retry_interval = 0.01 + config.retry_backoff_factor = 0.1 + config.retry_interval_randomness = 0.01 config.model_registry_class = 'Model' end diff --git a/spec/support/streaming_error_helpers.rb b/spec/support/streaming_error_helpers.rb index 9c89ef9c5..ba7568816 100644 --- a/spec/support/streaming_error_helpers.rb +++ b/spec/support/streaming_error_helpers.rb @@ -154,6 +154,10 @@ def expected_error_for(provider) ERROR_HANDLING_CONFIGS[provider][:expected_error] end + def expected_url_for(provider) + ERROR_HANDLING_CONFIGS[provider][:url] + end + def stub_error_response(provider, type) config = ERROR_HANDLING_CONFIGS[provider] return unless config