Compare commits

..

No commits in common. "main" and "v2.13.0" have entirely different histories.

13 changed files with 32 additions and 388 deletions

View File

@ -24,7 +24,7 @@ These resources can help:
This project attempts to improve in these areas. Join us in doing that important work.
If you want to privately raise any breach to this policy with the Faraday team, feel free to reach out to [@iMacTia](https://ruby.social/@iMacTia) and [@olleolleolle](https://ruby.social/@olleolleolle) on the Mastodon instance ruby.social.
If you want to privately raise any breach to this policy with the Faraday team, feel free to reach out to [@iMacTia](https://twitter.com/iMacTia) and [@olleolleolle](https://twitter.com/olleolleolle) on Twitter.
### Required Checks

View File

@ -43,7 +43,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: [ '3.0', '3.1', '3.2', '3.3', '3.4' ]
ruby: [ '3.0', '3.1', '3.2', '3.3' ]
experimental: [false]
include:
- ruby: head

View File

@ -2,7 +2,6 @@
* [Quick Start](getting-started/quick-start.md)
* [The Env Object](getting-started/env-object.md)
* [Dealing with Errors](getting-started/errors.md)
* [Migrating from rest-client](getting-started/rest-client-migration.md)
* Customization
* [Configuration](customization/index.md)
* [Connection Options](customization/connection-options.md)

View File

@ -1,225 +0,0 @@
# Migrating from `rest-client` to `Faraday`
The `rest-client` gem is in maintenance mode, and developers are encouraged to migrate to actively maintained alternatives like [`faraday`](https://github.com/lostisland/faraday). This guide highlights common usage patterns in `rest-client` and how to migrate them to `faraday`.
---
## Quick Comparison
| Task | rest-client example | faraday example |
| ----------------- | -------------------------------------------------------- | -------------------------------------------------------------------------- |
| Simple GET | `RestClient.get("https://httpbingo.org/get")` | `Faraday.get("https://httpbingo.org/get")` |
| GET with params | `RestClient.get(url, params: { id: 1 })` | `Faraday.get(url, { id: 1 })` |
| POST form data | `RestClient.post(url, { a: 1 })` | `Faraday.post(url, { a: 1 })` |
| POST JSON | `RestClient.post(url, obj.to_json, content_type: :json)` | `Faraday.post(url, obj.to_json, { 'Content-Type' => 'application/json' })` |
| Custom headers | `RestClient.get(url, { Authorization: 'Bearer token' })` | `Faraday.get(url, nil, { 'Authorization' => 'Bearer token' })` |
| Get response body | `response.body` | `response.body` |
| Get status code | `response.code` | `response.status` |
| Get headers | `response.headers` (returns `Hash<Symbol, String>`) | `response.headers` (returns `Hash<String, String>`) |
---
## Installation
In your `Gemfile`, replace `rest-client` with:
```ruby
gem "faraday"
```
Then run:
```sh
bundle install
```
---
## Basic HTTP Requests
### GET request
**rest-client:**
```ruby
RestClient.get("https://httpbingo.org/get")
```
**faraday:**
```ruby
Faraday.get("https://httpbingo.org/get")
```
---
### GET with Params
**rest-client:**
```ruby
RestClient.get("https://httpbingo.org/get", params: { id: 1, foo: "bar" })
```
**faraday:**
```ruby
Faraday.get("https://httpbingo.org/get", { id: 1, foo: "bar" })
```
---
### POST Requests
**rest-client:**
```ruby
RestClient.post("https://httpbingo.org/post", { foo: "bar" })
```
**faraday:**
```ruby
Faraday.post("https://httpbingo.org/post", { foo: "bar" })
```
---
### Sending JSON
**rest-client:**
```ruby
RestClient.post("https://httpbingo.org/post", { foo: "bar" }.to_json, content_type: :json)
```
**faraday (manual):**
```ruby
Faraday.post("https://httpbingo.org/post", { foo: "bar" }.to_json, { 'Content-Type' => 'application/json' })
```
**faraday (with middleware):**
```ruby
conn = Faraday.new(url: "https://httpbingo.org") do |f|
f.request :json # encode request body as JSON and set Content-Type
f.response :json # parse response body as JSON
end
conn.post("/post", { foo: "bar" })
```
---
## Handling Responses
**rest-client:**
```ruby
response = RestClient.get("https://httpbingo.org/headers")
response.code # => 200
response.body # => "..."
response.headers # => { content_type: "application/json", ... }
```
**faraday:**
> notice headers Hash keys are stringified, not symbolized like in rest-client
```ruby
response = Faraday.get("https://httpbingo.org/headers")
response.status # => 200
response.body # => "..."
response.headers # => { "content-type" => "application/json", ... }
```
---
## Error Handling
**rest-client:**
```ruby
begin
RestClient.get("https://httpbingo.org/status/404")
rescue RestClient::NotFound => e
puts e.response.code # 404
end
```
**faraday:**
> By default, Faraday does **not** raise exceptions for HTTP errors (like 404 or 500); it simply returns the response. If you want exceptions to be raised on HTTP error responses, include the `:raise_error` middleware.
>
> With `:raise_error`, Faraday will raise `Faraday::ResourceNotFound` for 404s and other exceptions for other 4xx/5xx responses.
>
> See also:
>
> * [Dealing with Errors](getting-started/errors.md)
> * [Raising Errors](middleware/included/raising-errors.md)
```ruby
conn = Faraday.new(url: "https://httpbingo.org") do |f|
f.response :raise_error
end
begin
conn.get("/status/404")
rescue Faraday::ResourceNotFound => e
puts e.response[:status] # 404
end
```
---
## Advanced Request Configuration
**rest-client:**
```ruby
RestClient::Request.execute(method: :get, url: "https://httpbingo.org/get", timeout: 10)
```
**faraday:**
```ruby
conn = Faraday.new(url: "https://httpbingo.org", request: { timeout: 10 })
conn.get("/get")
```
---
## Headers
**rest-client:**
```ruby
RestClient.get("https://httpbingo.org/headers", { Authorization: "Bearer token" })
```
**faraday:**
> Notice headers Hash expects stringified keys.
```ruby
Faraday.get("https://httpbingo.org/headers", nil, { "Authorization" => "Bearer token" })
```
---
## Redirects
**rest-client:**
Automatically follows GET/HEAD redirects by default.
**faraday:**
Use the `follow_redirects` middleware (not included by default):
```ruby
require "faraday/follow_redirects"
conn = Faraday.new(url: "https://httpbingo.org") do |f|
f.response :follow_redirects
end
```

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'cgi/escape'
require 'cgi/util' if RUBY_VERSION < '3.5'
require 'cgi'
require 'date'
require 'set'
require 'forwardable'

View File

@ -79,46 +79,12 @@ module Faraday
# Pulls out potential parent exception and response hash.
def exc_msg_and_response(exc, response = nil)
case exc
when Exception
[exc, exc.message, response]
when Hash
[nil, build_error_message_from_hash(exc), exc]
when Faraday::Env
[nil, build_error_message_from_env(exc), exc]
else
[nil, exc.to_s, response]
end
end
return [exc, exc.message, response] if exc.respond_to?(:backtrace)
private
return [nil, "the server responded with status #{exc[:status]}", exc] \
if exc.respond_to?(:each_key)
def build_error_message_from_hash(hash)
# Be defensive with external Hash objects - they might be missing keys
status = hash.fetch(:status, nil)
request = hash.fetch(:request, nil)
return fallback_error_message(status) if request.nil?
method = request.fetch(:method, nil)
url = request.fetch(:url, nil)
build_status_error_message(status, method, url)
end
def build_error_message_from_env(env)
# Faraday::Env is internal - we can make reasonable assumptions about its structure
build_status_error_message(env.status, env.method, env.url)
end
def build_status_error_message(status, method, url)
method_str = method ? method.to_s.upcase : ''
url_str = url ? url.to_s : ''
"the server responded with status #{status} for #{method_str} #{url_str}"
end
def fallback_error_message(status)
"the server responded with status #{status} - method and url are not available " \
'due to include_request: false on Faraday::Response::RaiseError middleware'
[nil, exc.to_s, response]
end
end

View File

@ -60,7 +60,7 @@ module Faraday
:reason_phrase, :response_body) do
const_set(:ContentLength, 'Content-Length')
const_set(:StatusesWithoutBody, Set.new([204, 304]))
const_set(:SuccessfulStatuses, 200..299)
const_set(:SuccessfulStatuses, (200..299))
# A Set of HTTP verbs that typically send a body. If no body is set for
# these requests, the Content-Length header is set to 0.

View File

@ -221,7 +221,7 @@ module Faraday
end
def raise_if_adapter(klass)
return unless klass <= Faraday::Adapter
return unless is_adapter?(klass)
raise 'Adapter should be set using the `adapter` method, not `use`'
end
@ -234,6 +234,10 @@ module Faraday
!@adapter.nil?
end
def is_adapter?(klass) # rubocop:disable Naming/PredicateName
klass <= Faraday::Adapter
end
def use_symbol(mod, key, ...)
use(mod.lookup_middleware(key), ...)
end

View File

@ -10,13 +10,11 @@ module Faraday
# lifecycle to a given Logger object. By default, this logs to STDOUT. See
# Faraday::Logging::Formatter to see specifically what is logged.
class Logger < Middleware
DEFAULT_OPTIONS = { formatter: Logging::Formatter }.merge(Logging::Formatter::DEFAULT_OPTIONS).freeze
def initialize(app, logger = nil, options = {})
super(app, options)
super(app)
logger ||= ::Logger.new($stdout)
formatter_class = @options.delete(:formatter)
@formatter = formatter_class.new(logger: logger, options: @options)
formatter_class = options.delete(:formatter) || Logging::Formatter
@formatter = formatter_class.new(logger: logger, options: options)
yield @formatter if block_given?
end

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true
module Faraday
VERSION = '2.13.4'
VERSION = '2.13.0'
end

View File

@ -23,7 +23,7 @@ RSpec.describe Faraday::Error do
it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to eq(exception) }
it { expect(subject.message).to eq('the server responded with status 400 - method and url are not available due to include_request: false on Faraday::Response::RaiseError middleware') }
it { expect(subject.message).to eq('the server responded with status 400') }
if RUBY_VERSION >= '3.4'
it { expect(subject.inspect).to eq('#<Faraday::Error response={status: 400}>') }
else
@ -89,87 +89,5 @@ RSpec.describe Faraday::Error do
it { expect(subject.response_headers).to eq(headers) }
it { expect(subject.response_body).to eq(body) }
end
context 'with hash missing status key' do
let(:exception) { { body: 'error body' } }
it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to eq(exception) }
it { expect(subject.message).to eq('the server responded with status - method and url are not available due to include_request: false on Faraday::Response::RaiseError middleware') }
end
context 'with hash with status but missing request data' do
let(:exception) { { status: 404, body: 'not found' } } # missing request key
it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to eq(exception) }
it { expect(subject.message).to eq('the server responded with status 404 - method and url are not available due to include_request: false on Faraday::Response::RaiseError middleware') }
end
context 'with hash with status and request but missing method in request' do
let(:exception) { { status: 404, body: 'not found', request: { url: 'http://example.com/test' } } } # missing method
it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to eq(exception) }
it { expect(subject.message).to eq('the server responded with status 404 for http://example.com/test') }
end
context 'with hash with status and request but missing url in request' do
let(:exception) { { status: 404, body: 'not found', request: { method: :get } } } # missing url
it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to eq(exception) }
it { expect(subject.message).to eq('the server responded with status 404 for GET ') }
end
context 'with properly formed Faraday::Env' do
# This represents the normal case - a well-formed Faraday::Env object
# with all the standard properties populated as they would be during
# a typical HTTP request/response cycle
let(:exception) { Faraday::Env.new }
before do
exception.status = 500
exception.method = :post
exception.url = URI('https://api.example.com/users')
exception.request = Faraday::RequestOptions.new
exception.response_headers = { 'content-type' => 'application/json' }
exception.response_body = '{"error": "Internal server error"}'
exception.request_headers = { 'authorization' => 'Bearer token123' }
exception.request_body = '{"name": "John"}'
end
it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to eq(exception) }
it { expect(subject.message).to eq('the server responded with status 500 for POST https://api.example.com/users') }
end
context 'with Faraday::Env missing status key' do
let(:exception) { Faraday::Env.new }
before do
exception[:body] = 'error body'
# Intentionally not setting status
end
it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to eq(exception) }
it { expect(subject.message).to eq('the server responded with status for ') }
end
context 'with Faraday::Env with direct method and url properties' do
let(:exception) { Faraday::Env.new }
before do
exception.status = 404
exception.method = :get
exception.url = URI('http://example.com/test')
exception[:body] = 'not found'
end
it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to eq(exception) }
it { expect(subject.message).to eq('the server responded with status 404 for GET http://example.com/test') }
end
end
end

View File

@ -189,7 +189,7 @@ RSpec.describe Faraday::Response::Logger do
context 'when logging request body' do
let(:logger_options) { { bodies: { request: true } } }
it 'logs only request body' do
it 'log only request body' do
conn.post '/ohyes', 'name=Tamago', accept: 'text/html'
expect(string_io.string).to match(%(name=Tamago))
expect(string_io.string).not_to match(%(pebbles))
@ -199,7 +199,7 @@ RSpec.describe Faraday::Response::Logger do
context 'when logging response body' do
let(:logger_options) { { bodies: { response: true } } }
it 'logs only response body' do
it 'log only response body' do
conn.post '/ohyes', 'name=Hamachi', accept: 'text/html'
expect(string_io.string).to match(%(pebbles))
expect(string_io.string).not_to match(%(name=Hamachi))
@ -209,13 +209,13 @@ RSpec.describe Faraday::Response::Logger do
context 'when logging request and response bodies' do
let(:logger_options) { { bodies: true } }
it 'logs request and response body' do
it 'log request and response body' do
conn.post '/ohyes', 'name=Ebi', accept: 'text/html'
expect(string_io.string).to match(%(name=Ebi))
expect(string_io.string).to match(%(pebbles))
end
it 'logs response body object' do
it 'log response body object' do
conn.get '/rubbles', nil, accept: 'text/html'
expect(string_io.string).to match(%([\"Barney\", \"Betty\", \"Bam Bam\"]\n))
end
@ -228,21 +228,6 @@ RSpec.describe Faraday::Response::Logger do
end
end
context 'when bodies are logged by default' do
before do
described_class.default_options = { bodies: true }
end
it 'logs response body' do
conn.post '/ohai'
expect(string_io.string).to match(%(fred))
end
after do
described_class.default_options = { bodies: false }
end
end
context 'when logging errors' do
let(:logger_options) { { errors: true } }

View File

@ -28,7 +28,7 @@ RSpec.describe Faraday::Response::RaiseError do
it 'raises Faraday::BadRequestError for 400 responses' do
expect { conn.get('bad-request') }.to raise_error(Faraday::BadRequestError) do |ex|
expect(ex.message).to eq('the server responded with status 400 for GET http:/bad-request')
expect(ex.message).to eq('the server responded with status 400')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(400)
expect(ex.response_status).to eq(400)
@ -39,7 +39,7 @@ RSpec.describe Faraday::Response::RaiseError do
it 'raises Faraday::UnauthorizedError for 401 responses' do
expect { conn.get('unauthorized') }.to raise_error(Faraday::UnauthorizedError) do |ex|
expect(ex.message).to eq('the server responded with status 401 for GET http:/unauthorized')
expect(ex.message).to eq('the server responded with status 401')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(401)
expect(ex.response_status).to eq(401)
@ -50,7 +50,7 @@ RSpec.describe Faraday::Response::RaiseError do
it 'raises Faraday::ForbiddenError for 403 responses' do
expect { conn.get('forbidden') }.to raise_error(Faraday::ForbiddenError) do |ex|
expect(ex.message).to eq('the server responded with status 403 for GET http:/forbidden')
expect(ex.message).to eq('the server responded with status 403')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(403)
expect(ex.response_status).to eq(403)
@ -61,7 +61,7 @@ RSpec.describe Faraday::Response::RaiseError do
it 'raises Faraday::ResourceNotFound for 404 responses' do
expect { conn.get('not-found') }.to raise_error(Faraday::ResourceNotFound) do |ex|
expect(ex.message).to eq('the server responded with status 404 for GET http:/not-found')
expect(ex.message).to eq('the server responded with status 404')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(404)
expect(ex.response_status).to eq(404)
@ -83,7 +83,7 @@ RSpec.describe Faraday::Response::RaiseError do
it 'raises Faraday::RequestTimeoutError for 408 responses' do
expect { conn.get('request-timeout') }.to raise_error(Faraday::RequestTimeoutError) do |ex|
expect(ex.message).to eq('the server responded with status 408 for GET http:/request-timeout')
expect(ex.message).to eq('the server responded with status 408')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(408)
expect(ex.response_status).to eq(408)
@ -94,7 +94,7 @@ RSpec.describe Faraday::Response::RaiseError do
it 'raises Faraday::ConflictError for 409 responses' do
expect { conn.get('conflict') }.to raise_error(Faraday::ConflictError) do |ex|
expect(ex.message).to eq('the server responded with status 409 for GET http:/conflict')
expect(ex.message).to eq('the server responded with status 409')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(409)
expect(ex.response_status).to eq(409)
@ -105,7 +105,7 @@ RSpec.describe Faraday::Response::RaiseError do
it 'raises Faraday::UnprocessableEntityError for 422 responses' do
expect { conn.get('unprocessable-entity') }.to raise_error(Faraday::UnprocessableEntityError) do |ex|
expect(ex.message).to eq('the server responded with status 422 for GET http:/unprocessable-entity')
expect(ex.message).to eq('the server responded with status 422')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(422)
expect(ex.response_status).to eq(422)
@ -116,7 +116,7 @@ RSpec.describe Faraday::Response::RaiseError do
it 'raises Faraday::TooManyRequestsError for 429 responses' do
expect { conn.get('too-many-requests') }.to raise_error(Faraday::TooManyRequestsError) do |ex|
expect(ex.message).to eq('the server responded with status 429 for GET http:/too-many-requests')
expect(ex.message).to eq('the server responded with status 429')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(429)
expect(ex.response_status).to eq(429)
@ -138,7 +138,7 @@ RSpec.describe Faraday::Response::RaiseError do
it 'raises Faraday::ClientError for other 4xx responses' do
expect { conn.get('4xx') }.to raise_error(Faraday::ClientError) do |ex|
expect(ex.message).to eq('the server responded with status 499 for GET http:/4xx')
expect(ex.message).to eq('the server responded with status 499')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(499)
expect(ex.response_status).to eq(499)
@ -149,7 +149,7 @@ RSpec.describe Faraday::Response::RaiseError do
it 'raises Faraday::ServerError for 500 responses' do
expect { conn.get('server-error') }.to raise_error(Faraday::ServerError) do |ex|
expect(ex.message).to eq('the server responded with status 500 for GET http:/server-error')
expect(ex.message).to eq('the server responded with status 500')
expect(ex.response[:headers]['X-Error']).to eq('bailout')
expect(ex.response[:status]).to eq(500)
expect(ex.response_status).to eq(500)