Skip to content

Commit

Permalink
Replace Faraday with the HTTP gem
Browse files Browse the repository at this point in the history
  • Loading branch information
sferik committed Jan 30, 2014
1 parent 88d7b19 commit fa0201d
Show file tree
Hide file tree
Showing 26 changed files with 64 additions and 304 deletions.
17 changes: 0 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,23 +467,6 @@ After configuration, requests can be made like so:
client.update("I'm tweeting with @gem!")
```

### Middleware
The Faraday middleware stack is fully configurable and is exposed as a
`Faraday::RackBuilder` object. You can modify the default middleware in-place:

```ruby
client.middleware.insert_after Twitter::Response::RaiseError, CustomMiddleware
```

A custom adapter may be set as part of a custom middleware stack:

```ruby
client.middleware = Faraday::RackBuilder.new do |faraday|
# Specify a middleware stack here
faraday.adapter :some_other_adapter
end
```

## Usage Examples
All examples require an authenticated Twitter client. See the section on <a
href="#configuration">configuration</a>.
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ end

require 'yardstick/rake/verify'
Yardstick::Rake::Verify.new do |verify|
verify.threshold = 59.7
verify.threshold = 59.9
end

task :default => [:spec, :rubocop, :verify_measurements]
14 changes: 0 additions & 14 deletions etc/erd.dot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ digraph classes {
ArgumentError [label="ArgumentError"]
Array [label="Array"]
Exception [label="Exception"]
Faraday__Middleware [label="Faraday::Middleware"]
Faraday__Response__Middleware [label="Faraday::Response::Middleware"]
Naught__BasicObject [label="Naught::BasicObject"]
StandardError [label="StandardError"]
Twitter__Arguments [label="Twitter::Arguments"]
Expand Down Expand Up @@ -34,7 +32,6 @@ digraph classes {
Twitter__Error__InternalServerError [label="Twitter::Error::InternalServerError"]
Twitter__Error__NotAcceptable [label="Twitter::Error::NotAcceptable"]
Twitter__Error__NotFound [label="Twitter::Error::NotFound"]
Twitter__Error__RequestTimeout [label="Twitter::Error::RequestTimeout"]
Twitter__Error__ServerError [label="Twitter::Error::ServerError"]
Twitter__Error__ServiceUnavailable [label="Twitter::Error::ServiceUnavailable"]
Twitter__Error__TooManyRequests [label="Twitter::Error::TooManyRequests"]
Expand All @@ -57,10 +54,6 @@ digraph classes {
Twitter__Place [label="Twitter::Place"]
Twitter__ProfileBanner [label="Twitter::ProfileBanner"]
Twitter__REST__Client [label="Twitter::REST::Client"]
Twitter__REST__Request__MultipartWithFile [label="Twitter::REST::Request::MultipartWithFile"]
Twitter__REST__Response__ParseErrorJson [label="Twitter::REST::Response::ParseErrorJson"]
Twitter__REST__Response__ParseJson [label="Twitter::REST::Response::ParseJson"]
Twitter__REST__Response__RaiseError [label="Twitter::REST::Response::RaiseError"]
Twitter__RateLimit [label="Twitter::RateLimit"]
Twitter__Relationship [label="Twitter::Relationship"]
Twitter__Request [label="Twitter::Request"]
Expand Down Expand Up @@ -88,8 +81,6 @@ digraph classes {
ArgumentError -> StandardError
Array -> Object
Exception -> Object
Faraday__Middleware -> Object
Faraday__Response__Middleware -> Faraday__Middleware
Naught__BasicObject -> Object
StandardError -> Exception
Twitter__Arguments -> Array
Expand Down Expand Up @@ -117,7 +108,6 @@ digraph classes {
Twitter__Error__InternalServerError -> Twitter__Error__ServerError
Twitter__Error__NotAcceptable -> Twitter__Error__ClientError
Twitter__Error__NotFound -> Twitter__Error__ClientError
Twitter__Error__RequestTimeout -> Twitter__Error__ClientError
Twitter__Error__ServerError -> Twitter__Error
Twitter__Error__ServiceUnavailable -> Twitter__Error__ServerError
Twitter__Error__TooManyRequests -> Twitter__Error__ClientError
Expand All @@ -140,10 +130,6 @@ digraph classes {
Twitter__Place -> Twitter__Base
Twitter__ProfileBanner -> Twitter__Base
Twitter__REST__Client -> Twitter__Client
Twitter__REST__Request__MultipartWithFile -> Faraday__Middleware
Twitter__REST__Response__ParseErrorJson -> Twitter__REST__Response__ParseJson
Twitter__REST__Response__ParseJson -> Faraday__Response__Middleware
Twitter__REST__Response__RaiseError -> Faraday__Response__Middleware
Twitter__RateLimit -> Twitter__Base
Twitter__Relationship -> Twitter__Base
Twitter__Request -> Object
Expand Down
Binary file modified etc/erd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion lib/twitter/cursor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def last?

# @return [Hash]
def fetch_next_page
response = @client.send(@request_method, @path, @options.merge(:cursor => next_cursor)).body
response = @client.send(@request_method, @path, @options.merge(:cursor => next_cursor))
self.attrs = response
end

Expand Down
12 changes: 4 additions & 8 deletions lib/twitter/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ module Code
class << self
# Create a new error from an HTTP response
#
# @param response [Faraday::Response]
# @param response [HTTP::Response]
# @return [Twitter::Error]
def from_response(response)
message, code = parse_error(response.body)
new(message, response.response_headers, code)
message, code = parse_error(response.parse)
new(message, response.headers, code)
end

# @return [Hash]
Expand All @@ -47,7 +47,6 @@ def errors
403 => Twitter::Error::Forbidden,
404 => Twitter::Error::NotFound,
406 => Twitter::Error::NotAcceptable,
408 => Twitter::Error::RequestTimeout,
422 => Twitter::Error::UnprocessableEntity,
429 => Twitter::Error::TooManyRequests,
500 => Twitter::Error::InternalServerError,
Expand All @@ -68,7 +67,7 @@ def forbidden_messages
private

def parse_error(body)
if body.nil?
if body.nil? || body.empty?
['', nil]
elsif body[:error]
[body[:error], nil]
Expand Down Expand Up @@ -129,9 +128,6 @@ class NotFound < ClientError; end
# Raised when Twitter returns the HTTP status code 406
class NotAcceptable < ClientError; end

# Raised when Twitter returns the HTTP status code 408
class RequestTimeout < ClientError; end

# Raised when Twitter returns the HTTP status code 422
class UnprocessableEntity < ClientError; end

Expand Down
2 changes: 1 addition & 1 deletion lib/twitter/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def initialize(client, request_method, path, options = {})

# @return [Hash]
def perform
@client.send(@request_method, @path, @options).body
@client.send(@request_method, @path, @options)
end

# @param klass [Class]
Expand Down
100 changes: 23 additions & 77 deletions lib/twitter/rest/client.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
require 'base64'
require 'faraday'
require 'faraday/request/multipart'
require 'http'
require 'json'
require 'timeout'
require 'twitter/client'
require 'twitter/error'
require 'twitter/rest/api'
require 'twitter/rest/request/multipart_with_file'
require 'twitter/rest/response/parse_json'
require 'twitter/rest/response/raise_error'

module Twitter
module REST
Expand All @@ -22,51 +18,16 @@ class Client < Twitter::Client
attr_writer :connection_options, :middleware
ENDPOINT = 'https://api.twitter.com'

def connection_options
@connection_options ||= {
:builder => middleware,
:headers => {
:accept => 'application/json',
:user_agent => user_agent,
},
:request => {
:open_timeout => 10,
:timeout => 30,
},
}
end

# @note Faraday's middleware stack implementation is comparable to that of Rack middleware. The order of middleware is important: the first middleware on the list wraps all others, while the last middleware is the innermost one.
# @see https://github.com/technoweenie/faraday#advanced-middleware-usage
# @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/
# @return [Faraday::RackBuilder]
def middleware
@middleware ||= Faraday::RackBuilder.new do |faraday|
# Convert file uploads to Faraday::UploadIO objects
faraday.request :multipart_with_file
# Checks for files in the payload, otherwise leaves everything untouched
faraday.request :multipart
# Encodes as "application/x-www-form-urlencoded" if not already encoded
faraday.request :url_encoded
# Handle error responses
faraday.response :raise_error
# Parse JSON response bodies
faraday.response :parse_json
# Set default HTTP adapter
faraday.adapter :net_http
end
end

# Perform an HTTP GET request
def get(path, params = {})
headers = request_headers(:get, path, params)
request(:get, path, params, headers)
header = auth_header(:get, path, params)
request(:get, path, {:params => params}, :authorization => header)
end

# Perform an HTTP POST request
def post(path, params = {})
headers = params.values.any? { |value| value.respond_to?(:to_io) } ? request_headers(:post, path, params, {}) : request_headers(:post, path, params)
request(:post, path, params, headers)
header = params.values.any? { |value| value.respond_to?(:to_io) } ? auth_header(:post, path, params, {}) : auth_header(:post, path, params)
request(:post, path, {:form => params}, :authorization => header)
end

# @return [Boolean]
Expand All @@ -81,32 +42,30 @@ def credentials?

private

# Returns a Faraday::Connection object
#
# @return [Faraday::Connection]
def connection
@connection ||= Faraday.new(ENDPOINT, connection_options)
def request(method, path, params = {}, headers = {})
response = HTTP.with(headers).send(method, ENDPOINT + path, params)
error = error(response)
fail(error) if error
response.parse
end

def request(method, path, params = {}, headers = {})
connection.send(method.to_sym, path, params) { |request| request.headers.update(headers) }.env
rescue Faraday::Error::TimeoutError, Timeout::Error => error
raise(Twitter::Error::RequestTimeout.new(error))
rescue Faraday::Error::ClientError, JSON::ParserError => error
fail(Twitter::Error.new(error))
def error(response)
klass = Twitter::Error.errors[response.code]
if klass == Twitter::Error::Forbidden
forbidden_error(response)
elsif !klass.nil?
klass.from_response(response)
end
end

def request_headers(method, path, params = {}, signature_params = params)
bearer_token_request = params.delete(:bearer_token_request)
headers = {}
if bearer_token_request
headers[:accept] = '*/*'
headers[:authorization] = bearer_token_credentials_auth_header
headers[:content_type] = 'application/x-www-form-urlencoded; charset=UTF-8'
def forbidden_error(response)
error = Twitter::Error::Forbidden.from_response(response)
klass = Twitter::Error.forbidden_messages[error.message]
if klass
klass.from_response(response)
else
headers[:authorization] = auth_header(method, path, params, signature_params)
error
end
headers
end

def auth_header(method, path, params = {}, signature_params = params)
Expand All @@ -118,23 +77,10 @@ def auth_header(method, path, params = {}, signature_params = params)
end
end

# Generates authentication header for a bearer token request
#
# @return [String]
def bearer_token_credentials_auth_header
basic_auth_token = strict_encode64("#{@consumer_key}:#{@consumer_secret}")
"Basic #{basic_auth_token}"
end

def bearer_auth_header
token = bearer_token.is_a?(Twitter::Token) && bearer_token.bearer? ? bearer_token.access_token : bearer_token
"Bearer #{token}"
end

# Base64.strict_encode64 is not available on Ruby 1.8.7
def strict_encode64(str)
Base64.encode64(str).gsub("\n", '')
end
end
end
end
2 changes: 1 addition & 1 deletion lib/twitter/rest/friends_and_followers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def friends(*args)
# @return [Array<Integer>]
# @param options [Hash] A customizable set of options.
def no_retweet_ids(options = {})
get('/1.1/friendships/no_retweets/ids.json', options).body.collect(&:to_i)
get('/1.1/friendships/no_retweets/ids.json', options).collect(&:to_i)
end
alias_method :no_retweets_ids, :no_retweet_ids
end
Expand Down
4 changes: 2 additions & 2 deletions lib/twitter/rest/help.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def languages(options = {})
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [String]
def privacy(options = {})
get('/1.1/help/privacy.json', options).body[:privacy]
get('/1.1/help/privacy.json', options)[:privacy]
end

# Returns {https://twitter.com/tos Twitter's Terms of Service}
Expand All @@ -49,7 +49,7 @@ def privacy(options = {})
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [String]
def tos(options = {})
get('/1.1/help/tos.json', options).body[:tos]
get('/1.1/help/tos.json', options)[:tos]
end
end
end
Expand Down
25 changes: 16 additions & 9 deletions lib/twitter/rest/oauth.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
require 'twitter/request'
require 'twitter/rest/utils'
require 'twitter/rest/response/parse_error_json'
require 'twitter/token'

module Twitter
Expand All @@ -24,9 +23,12 @@ module OAuth
# client = Twitter::REST::Client.new(:consumer_key => "abc", :consumer_secret => 'def')
# bearer_token = client.token
def token(options = {})
options[:bearer_token_request] = true
options[:grant_type] ||= 'client_credentials'
perform_with_object(:post, '/oauth2/token', options, Twitter::Token)
headers = {}
headers[:accept] = '*/*'
headers[:authorization] = "Basic #{strict_encode64("#{@consumer_key}:#{@consumer_secret}")}"
response = HTTP.with(headers).post('https://api.twitter.com/oauth2/token', :form => options)
Twitter::Token.new(response.parse)
end
alias_method :bearer_token, :token

Expand All @@ -53,12 +55,17 @@ def invalidate_token(access_token, options = {})
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [String] The token string.
def reverse_token
conn = connection.dup
conn.builder.swap(4, Twitter::REST::Response::ParseErrorJson)
response = conn.post('/oauth/request_token?x_auth_mode=reverse_auth') do |request|
request.headers[:authorization] = oauth_auth_header(:post, 'https://api.twitter.com/oauth/request_token', :x_auth_mode => 'reverse_auth').to_s
end
response.body
uri = 'https://api.twitter.com/oauth/request_token'
options = {:x_auth_mode => 'reverse_auth'}
headers = {:authorization => oauth_auth_header(:post, uri, options).to_s}
HTTP.with(headers).post(uri, :params => options).to_s
end

private

# Base64.strict_encode64 is not available on Ruby 1.8.7
def strict_encode64(str)
Base64.encode64(str).gsub("\n", '')
end
end
end
Expand Down
Loading

4 comments on commit fa0201d

@stve
Copy link
Collaborator

@stve stve commented on fa0201d Jan 30, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work! 🎉

@sferik
Copy link
Owner Author

@sferik sferik commented on fa0201d Jan 30, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spagalloco The specs run about 4X faster! Unfortunately, multipart POST (i.e. upload_with_media) isn’t working. 😞

Would love to get your help on this, if you’re interested.

Also, what do you think about removing the middleware stack? Do you think many people use custom middleware? I think the most popular use-case is proxy middleware. No idea how many people are using this. I suppose proxying could happen at another layer but I suppose this would require a major version bump.

@stve
Copy link
Collaborator

@stve stve commented on fa0201d Jan 31, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom middleware was a nifty feature, but more a bi-product of using Faraday. Like you, I'd question whether anyone actually used it much.

Happy to help with upload_with_media, I'll take a look.

@sferik
Copy link
Owner Author

@sferik sferik commented on fa0201d Jan 31, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like you, I'd question whether anyone actually used it much.

I looked back through existing issues that contain "middleware". The couple I found were #354 and #369, which seem to just be swapping out the adapter to work around a bug in net/http. Hopefully, the HTTP gem will not have these issues. Even if it has similar (or different) issues, they should be easier to fix, since we can patch and push a new gem instead of having to wait for a new version of Ruby to be released.

Another option is writing a Faraday adapter for the HTTP gem and making that the default in a 5.x release, before we merge this patch and release version 6.

Please sign in to comment.