From 50b6e2841de2cac120b29c1d9a73268b7c049930 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 5 Oct 2011 23:00:39 -0700 Subject: [PATCH 1/5] Added retry and timeout middleware --- lib/faraday/request.rb | 8 ++++++-- lib/faraday/request/retry.rb | 21 +++++++++++++++++++++ lib/faraday/request/timeout.rb | 16 ++++++++++++++++ test/middleware/retry_test.rb | 25 +++++++++++++++++++++++++ test/middleware/timeout_test.rb | 20 ++++++++++++++++++++ 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 lib/faraday/request/retry.rb create mode 100644 lib/faraday/request/timeout.rb create mode 100644 test/middleware/retry_test.rb create mode 100644 test/middleware/timeout_test.rb diff --git a/lib/faraday/request.rb b/lib/faraday/request.rb index bc3c32f7..115bf6f3 100644 --- a/lib/faraday/request.rb +++ b/lib/faraday/request.rb @@ -15,12 +15,16 @@ module Faraday autoload_all 'faraday/request', :JSON => 'json', :UrlEncoded => 'url_encoded', - :Multipart => 'multipart' + :Multipart => 'multipart', + :Retry => 'retry', + :Timeout => 'timeout' register_lookup_modules \ :json => :JSON, :url_encoded => :UrlEncoded, - :multipart => :Multipart + :multipart => :Multipart, + :retry => :Retry, + :timeout => :Timeout attr_reader :method diff --git a/lib/faraday/request/retry.rb b/lib/faraday/request/retry.rb new file mode 100644 index 00000000..b44abd63 --- /dev/null +++ b/lib/faraday/request/retry.rb @@ -0,0 +1,21 @@ +module Faraday + class Request::Retry < Faraday::Middleware + def initialize(app, retries = 2) + @retries = retries + super(app) + end + + def call(env) + retries = @retries + begin + @app.call(env) + rescue Exception => e + if retries > 0 + retries -= 1 + retry + end + raise + end + end + end +end diff --git a/lib/faraday/request/timeout.rb b/lib/faraday/request/timeout.rb new file mode 100644 index 00000000..d284c559 --- /dev/null +++ b/lib/faraday/request/timeout.rb @@ -0,0 +1,16 @@ +module Faraday + class Request::Timeout < Faraday::Middleware + dependency "timeout" + + def initialize(app, timeout = 2) + @timeout = timeout + super(app) + end + + def call(env) + Timeout::timeout(@timeout) do + @app.call(env) + end + end + end +end diff --git a/test/middleware/retry_test.rb b/test/middleware/retry_test.rb new file mode 100644 index 00000000..53d2d88f --- /dev/null +++ b/test/middleware/retry_test.rb @@ -0,0 +1,25 @@ +require File.expand_path(File.join(File.dirname(__FILE__), "..", "helper")) + +module Middleware + class RetryTest < Faraday::TestCase + def setup + @stubs = Faraday::Adapter::Test::Stubs.new + @conn = Faraday.new do |b| + b.request :retry, 2 + b.adapter :test, @stubs + end + end + + def test_retries + times_called = 0 + + @stubs.post("/echo") do + times_called += 1 + raise "Error occurred" + end + + @conn.post("/echo") rescue nil + assert_equal times_called, 3 + end + end +end diff --git a/test/middleware/timeout_test.rb b/test/middleware/timeout_test.rb new file mode 100644 index 00000000..c667084b --- /dev/null +++ b/test/middleware/timeout_test.rb @@ -0,0 +1,20 @@ +require File.expand_path(File.join(File.dirname(__FILE__), "..", "helper")) + +module Middleware + class TimeoutTest < Faraday::TestCase + def setup + @conn = Faraday.new do |b| + b.request :timeout, 0.01 # 10 ms + b.adapter :test do |stub| + stub.post("/echo") do |env| + sleep(1) + end + end + end + end + + def test_request_times_out + assert_raise(TimeoutError) { @conn.post("/echo") } + end + end +end From e3c1091de812f566de1f004ca5f3951358dc24ab Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 6 Oct 2011 01:09:28 -0700 Subject: [PATCH 2/5] Only rescue from StandardError's --- lib/faraday/request/retry.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/faraday/request/retry.rb b/lib/faraday/request/retry.rb index b44abd63..56a80d9b 100644 --- a/lib/faraday/request/retry.rb +++ b/lib/faraday/request/retry.rb @@ -9,7 +9,7 @@ module Faraday retries = @retries begin @app.call(env) - rescue Exception => e + rescue => e if retries > 0 retries -= 1 retry From 882ded46c1623c50967c029e7e074eda7e1913ab Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 6 Oct 2011 01:33:20 -0700 Subject: [PATCH 3/5] Rescue Timeout::Error for Ruby 1.8.7 --- lib/faraday/request/retry.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/faraday/request/retry.rb b/lib/faraday/request/retry.rb index 56a80d9b..6bb3ee0d 100644 --- a/lib/faraday/request/retry.rb +++ b/lib/faraday/request/retry.rb @@ -9,7 +9,7 @@ module Faraday retries = @retries begin @app.call(env) - rescue => e + rescue StandardError, Timeout::Error => e if retries > 0 retries -= 1 retry From 21bbebaf1c11f2f6e341dc7c9ca1fe3bde8ee7c3 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 6 Oct 2011 12:27:01 -0700 Subject: [PATCH 4/5] Use SystemTimer if possible in Ruby 1.8 --- lib/faraday/request/timeout.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/faraday/request/timeout.rb b/lib/faraday/request/timeout.rb index d284c559..2a79a874 100644 --- a/lib/faraday/request/timeout.rb +++ b/lib/faraday/request/timeout.rb @@ -3,14 +3,29 @@ module Faraday dependency "timeout" def initialize(app, timeout = 2) + self.class.dependency "system_timer" if ruby18? @timeout = timeout super(app) end def call(env) - Timeout::timeout(@timeout) do + method = + if ruby18? && self.class.loaded? + SystemTimer.method(:timeout_after) + else + Timeout.method(:timeout) + end + + method.call(@timeout) do @app.call(env) end end + + private + + def ruby18? + @ruby18 ||= RUBY_VERSION =~ /^1\.8/ + end + end end From 3a52825d57776978f509fde42df08791181b6228 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 16 Oct 2011 16:42:12 -0700 Subject: [PATCH 5/5] Added options documentation and syntax highlighting to README --- README.md | 204 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 117 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 381a1dbd..db92fe07 100644 --- a/README.md +++ b/README.md @@ -6,62 +6,84 @@ This mess is gonna get raw, like sushi. So, haters to the left. Usage ----- - conn = Faraday.new(:url => 'http://sushi.com') do |builder| - builder.use Faraday::Request::UrlEncoded # convert request params as "www-form-urlencoded" - builder.use Faraday::Request::JSON # encode request params as json - builder.use Faraday::Response::Logger # log the request to STDOUT - builder.use Faraday::Adapter::NetHttp # make http requests with Net::HTTP - # or, use shortcuts: - builder.request :url_encoded - builder.request :json - builder.response :logger - builder.adapter :net_http - end +```ruby +conn = Faraday.new(:url => 'http://sushi.com') do |builder| + builder.use Faraday::Request::UrlEncoded # convert request params as "www-form-urlencoded" + builder.use Faraday::Request::JSON # encode request params as json + builder.use Faraday::Response::Logger # log the request to STDOUT + builder.use Faraday::Adapter::NetHttp # make http requests with Net::HTTP - ## GET ## + # or, use shortcuts: + builder.request :url_encoded + builder.request :json + builder.response :logger + builder.adapter :net_http +end - response = conn.get '/nigiri/sake.json' # GET http://sushi.com/nigiri/sake.json - response.body +## GET ## - conn.get '/nigiri', 'X-Awesome' => true # custom request header +response = conn.get '/nigiri/sake.json' # GET http://sushi.com/nigiri/sake.json +response.body - conn.get do |req| # GET http://sushi.com/search?page=2&limit=100 - req.url '/search', :page => 2 - req.params['limit'] = 100 - end +conn.get '/nigiri', 'X-Awesome' => true # custom request header - ## POST ## +conn.get do |req| # GET http://sushi.com/search?page=2&limit=100 + req.url '/search', :page => 2 + req.params['limit'] = 100 +end - conn.post '/nigiri', { :name => 'Maguro' } # POST "name=maguro" to http://sushi.com/nigiri +## POST ## - # post payload as JSON instead of "www-form-urlencoded" encoding: - conn.post '/nigiri', payload, 'Content-Type' => 'application/json' +conn.post '/nigiri', { :name => 'Maguro' } # POST "name=maguro" to http://sushi.com/nigiri - # a more verbose way: - conn.post do |req| - req.url '/nigiri' - req.headers['Content-Type'] = 'application/json' - req.body = { :name => 'Unagi' } - end +# post payload as JSON instead of "www-form-urlencoded" encoding: +conn.post '/nigiri', payload, 'Content-Type' => 'application/json' + +# a more verbose way: +conn.post do |req| + req.url '/nigiri' + req.headers['Content-Type'] = 'application/json' + req.body = { :name => 'Unagi' } +end + +## Options ## + +conn.get do |req| + req.url '/search' + req.options = { + :timeout => 5, # open/read timeout Integer in seconds + :open_timeout => 2, # read timeout Integer in seconds + :proxy => { + :uri => "http://example.org/", # proxy server URI + :user => "me", # proxy server username + :password => "test123" # proxy server password + } + } +end +``` If you're ready to roll with just the bare minimum: - # default stack (net/http), no extra middleware: - response = Faraday.get 'http://sushi.com/nigiri/sake.json' +```ruby +# default stack (net/http), no extra middleware: +response = Faraday.get 'http://sushi.com/nigiri/sake.json' +``` Advanced middleware usage ------------------------- The order in which middleware is stacked is important. Like with Rack, the first middleware on the list wraps all others, while the last middleware is the innermost one, so that's usually the adapter. - conn = Faraday.new(:url => 'http://sushi.com') do |builder| - # POST/PUT params encoders: - builder.request :multipart - builder.request :url_encoded - builder.request :json +```ruby +conn = Faraday.new(:url => 'http://sushi.com') do |builder| + # POST/PUT params encoders: + builder.request :multipart + builder.request :url_encoded + builder.request :json - builder.adapter :net_http - end + builder.adapter :net_http +end +``` This request middleware setup affects POST/PUT requests in the following way: @@ -73,82 +95,90 @@ Because "UrlEncoded" is higher on the stack than JSON encoder, it will get to pr Examples: - payload = { :name => 'Maguro' } +```ruby +payload = { :name => 'Maguro' } - # post payload as JSON instead of urlencoded: - conn.post '/nigiri', payload, 'Content-Type' => 'application/json' +# post payload as JSON instead of urlencoded: +conn.post '/nigiri', payload, 'Content-Type' => 'application/json' - # uploading a file: - payload = { :profile_pic => Faraday::UploadIO.new('avatar.jpg', 'image/jpeg') } +# uploading a file: +payload = { :profile_pic => Faraday::UploadIO.new('avatar.jpg', 'image/jpeg') } - # "Multipart" middleware detects files and encodes with "multipart/form-data": - conn.put '/profile', payload +# "Multipart" middleware detects files and encodes with "multipart/form-data": +conn.put '/profile', payload +``` Writing middleware ------------------ Middleware are classes that respond to `call()`. They wrap the request/response cycle. - def call(env) - # do something with the request +```ruby +def call(env) + # do something with the request - @app.call(env).on_complete do - # do something with the response - end - end + @app.call(env).on_complete do + # do something with the response + end +end +``` It's important to do all processing of the response only in the `on_complete` block. This enables middleware to work in parallel mode where requests are asynchronous. The `env` is a hash with symbol keys that contains info about the request and, later, response. Some keys are: - # request phase - :method - :get, :post, ... - :url - URI for the current request; also contains GET parameters - :body - POST parameters for :post/:put requests - :request_headers +``` +# request phase +:method - :get, :post, ... +:url - URI for the current request; also contains GET parameters +:body - POST parameters for :post/:put requests +:request_headers - # response phase - :status - HTTP response status code, such as 200 - :body - the response body - :response_headers +# response phase +:status - HTTP response status code, such as 200 +:body - the response body +:response_headers +``` Testing ------- - # It's possible to define stubbed request outside a test adapter block. - stubs = Faraday::Adapter::Test::Stubs.new do |stub| - stub.get('/tamago') { [200, {}, 'egg'] } - end - # You can pass stubbed request to the test adapter or define them in a block - # or a combination of the two. - test = Faraday.new do |builder| - builder.adapter :test, stubs do |stub| - stub.get('/ebi') {[ 200, {}, 'shrimp' ]} - end - end +```ruby +# It's possible to define stubbed request outside a test adapter block. +stubs = Faraday::Adapter::Test::Stubs.new do |stub| + stub.get('/tamago') { [200, {}, 'egg'] } +end - # It's also possible to stub additional requests after the connection has - # been initialized. This is useful for testing. - stubs.get('/uni') {[ 200, {}, 'urchin' ]} +# You can pass stubbed request to the test adapter or define them in a block +# or a combination of the two. +test = Faraday.new do |builder| + builder.adapter :test, stubs do |stub| + stub.get('/ebi') {[ 200, {}, 'shrimp' ]} + end +end - resp = test.get '/tamago' - resp.body # => 'egg' - resp = test.get '/ebi' - resp.body # => 'shrimp' - resp = test.get '/uni' - resp.body # => 'urchin' - resp = test.get '/else' #=> raises "no such stub" error +# It's also possible to stub additional requests after the connection has +# been initialized. This is useful for testing. +stubs.get('/uni') {[ 200, {}, 'urchin' ]} - # If you like, you can treat your stubs as mocks by verifying that all of - # the stubbed calls were made. NOTE that this feature is still fairly - # experimental: It will not verify the order or count of any stub, only that - # it was called once during the course of the test. - stubs.verify_stubbed_calls +resp = test.get '/tamago' +resp.body # => 'egg' +resp = test.get '/ebi' +resp.body # => 'shrimp' +resp = test.get '/uni' +resp.body # => 'urchin' +resp = test.get '/else' #=> raises "no such stub" error + +# If you like, you can treat your stubs as mocks by verifying that all of +# the stubbed calls were made. NOTE that this feature is still fairly +# experimental: It will not verify the order or count of any stub, only that +# it was called once during the course of the test. +stubs.verify_stubbed_calls +``` TODO ---- * support streaming requests/responses * better stubbing API -* Support timeouts * Add curb, em-http, fast_http Note on Patches/Pull Requests