├── .rspec ├── .document ├── tests ├── rackups │ ├── basic.ru │ ├── deflater.ru │ ├── timeout.ru │ ├── query_string.ru │ ├── request_headers.ru │ ├── request_methods.ru │ ├── thread_safety.ru │ ├── response_header.ru │ ├── basic_auth.ru │ ├── proxy.ru │ ├── redirecting.ru │ ├── ssl.ru │ ├── ssl_mismatched_cn.ru │ ├── ssl_verify_peer.ru │ ├── streaming.ru │ ├── basic.rb │ ├── redirecting_with_cookie.ru │ └── webrick_patch.rb ├── data │ ├── xs │ ├── excon.cert.crt │ ├── 127.0.0.1.cert.crt │ ├── excon.cert.key │ └── 127.0.0.1.cert.key ├── servers │ ├── eof.rb │ ├── bad.rb │ └── error.rb ├── timeout_tests.rb ├── request_headers_tests.rb ├── authorization_header_tests.rb ├── middlewares │ ├── canned_response_tests.rb │ ├── capture_cookies_tests.rb │ ├── escape_path_tests.rb │ ├── redirect_follower_tests.rb │ ├── idempotent_tests.rb │ └── decompress_tests.rb ├── complete_responses.rb ├── request_method_tests.rb ├── thread_safety_tests.rb ├── bad_tests.rb ├── pipeline_tests.rb ├── request_tests.rb ├── utils_tests.rb ├── query_string_tests.rb ├── header_tests.rb ├── error_tests.rb └── response_tests.rb ├── .gitignore ├── spec ├── excon_spec.rb ├── support │ ├── shared_examples │ │ ├── shared_example_for_test_servers.rb │ │ ├── shared_example_for_streaming_clients.rb │ │ └── shared_example_for_clients.rb │ └── shared_contexts │ │ └── test_server_context.rb ├── helpers │ └── file_path_helpers.rb ├── spec_helper.rb ├── excon │ ├── test │ │ └── server_spec.rb │ └── error_spec.rb └── requests │ ├── unix_socket_spec.rb │ ├── basic_spec.rb │ └── eof_requests_spec.rb ├── lib └── excon │ ├── middlewares │ ├── escape_path.rb │ ├── response_parser.rb │ ├── base.rb │ ├── expects.rb │ ├── capture_cookies.rb │ ├── instrumentor.rb │ ├── idempotent.rb │ ├── decompress.rb │ ├── mock.rb │ └── redirect_follower.rb │ ├── test │ ├── plugin │ │ └── server │ │ │ ├── puma.rb │ │ │ ├── exec.rb │ │ │ ├── webrick.rb │ │ │ └── unicorn.rb │ └── server.rb │ ├── standard_instrumentor.rb │ ├── unix_socket.rb │ ├── extensions │ └── uri.rb │ ├── pretty_printer.rb │ ├── headers.rb │ ├── utils.rb │ ├── constants.rb │ ├── ssl_socket.rb │ └── response.rb ├── benchmarks ├── cr_lf.rb ├── single_vs_double_quotes.rb ├── concat_vs_insert.rb ├── merging.rb ├── concat_vs_interpolate.rb ├── for_vs_array_each.rb ├── for_vs_hash_each.rb ├── headers_split_vs_match.rb ├── class_vs_lambda.rb ├── excon.rb ├── vs_stdlib.rb ├── headers_case_sensitivity.rb ├── string_ranged_index.rb ├── implicit_block-vs-explicit_block.rb ├── strip_newline.rb ├── excon_vs.rb ├── downcase-eq-eq_vs_casecmp.rb └── has_key-vs-lookup.rb ├── .travis.yml ├── Gemfile ├── LICENSE.md ├── CONTRIBUTING.md ├── Rakefile ├── CONTRIBUTORS.md ├── excon.gemspec └── Gemfile.lock /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --format d 4 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /tests/rackups/basic.ru: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'basic') 2 | 3 | run Basic 4 | -------------------------------------------------------------------------------- /tests/data/xs: -------------------------------------------------------------------------------- 1 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 2 | -------------------------------------------------------------------------------- /tests/rackups/deflater.ru: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'basic') 2 | 3 | use Rack::Deflater 4 | run Basic 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rbc 2 | *.sw? 3 | .bundle 4 | .DS_Store 5 | .yardoc 6 | .rvmrc 7 | .ruby-version 8 | .ruby-gemset 9 | coverage 10 | doc 11 | rdoc 12 | pkg 13 | -------------------------------------------------------------------------------- /spec/excon_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Excon do 4 | it 'has a version number' do 5 | expect(Excon::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /tests/rackups/timeout.ru: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require File.join(File.dirname(__FILE__), 'webrick_patch') 3 | 4 | class App < Sinatra::Base 5 | set :environment, :production 6 | enable :dump_errors 7 | 8 | get('/timeout') do 9 | sleep(2) 10 | '' 11 | end 12 | end 13 | 14 | run App 15 | -------------------------------------------------------------------------------- /tests/rackups/query_string.ru: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require File.join(File.dirname(__FILE__), 'webrick_patch') 3 | 4 | class App < Sinatra::Base 5 | set :environment, :production 6 | enable :dump_errors 7 | 8 | get('/query') do 9 | "query: " << request.query_string 10 | end 11 | end 12 | 13 | run App 14 | -------------------------------------------------------------------------------- /tests/servers/eof.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "eventmachine" 4 | 5 | module EOFServer 6 | def receive_data(data) 7 | case data 8 | when %r{^GET /eof\s} 9 | close_connection(true) 10 | end 11 | end 12 | end 13 | 14 | EM.run do 15 | EM.start_server("127.0.0.1", 9292, EOFServer) 16 | $stderr.puts "ready" 17 | end 18 | -------------------------------------------------------------------------------- /tests/rackups/request_headers.ru: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require File.join(File.dirname(__FILE__), 'webrick_patch') 3 | 4 | class App < Sinatra::Base 5 | set :environment, :production 6 | enable :dump_errors 7 | 8 | post '/' do 9 | h = "" 10 | env.each { |k,v| h << "#{$1.downcase}: #{v}\n" if k =~ /http_(.*)/i } 11 | h 12 | end 13 | end 14 | 15 | run App 16 | -------------------------------------------------------------------------------- /tests/rackups/request_methods.ru: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require File.join(File.dirname(__FILE__), 'webrick_patch') 3 | 4 | class App < Sinatra::Base 5 | set :environment, :production 6 | enable :dump_errors 7 | 8 | get '/' do 9 | 'GET' 10 | end 11 | 12 | post '/' do 13 | 'POST' 14 | end 15 | 16 | delete '/' do 17 | 'DELETE' 18 | end 19 | end 20 | 21 | run App 22 | -------------------------------------------------------------------------------- /tests/rackups/thread_safety.ru: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require File.join(File.dirname(__FILE__), 'webrick_patch') 3 | 4 | class App < Sinatra::Base 5 | set :environment, :production 6 | enable :dump_errors 7 | 8 | get('/id/:id/wait/:wait') do |id, wait| 9 | sleep(wait.to_i) 10 | id.to_s 11 | end 12 | end 13 | 14 | # get everything autoloaded, mainly for rbx 15 | App.new 16 | 17 | run App 18 | -------------------------------------------------------------------------------- /lib/excon/middlewares/escape_path.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | module Middleware 4 | class EscapePath < Excon::Middleware::Base 5 | def request_call(datum) 6 | # make sure path is encoded, prevent double encoding 7 | datum[:path] = Excon::Utils.escape_uri(Excon::Utils.unescape_uri(datum[:path])) 8 | @stack.request_call(datum) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/excon/middlewares/response_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | module Middleware 4 | class ResponseParser < Excon::Middleware::Base 5 | def response_call(datum) 6 | unless datum.has_key?(:response) 7 | datum = Excon::Response.parse(datum[:connection].send(:socket), datum) 8 | end 9 | @stack.response_call(datum) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /benchmarks/cr_lf.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'tach' 3 | 4 | CR_LF = "\r\n" 5 | 6 | Tach.meter(1_000_000) do 7 | tach('constant') do 8 | '' << CR_LF 9 | end 10 | tach('string') do 11 | '' << "\r\n" 12 | end 13 | end 14 | 15 | # +----------+----------+ 16 | # | tach | total | 17 | # +----------+----------+ 18 | # | constant | 0.819885 | 19 | # +----------+----------+ 20 | # | string | 0.893602 | 21 | # +----------+----------+ -------------------------------------------------------------------------------- /tests/timeout_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('read should timeout') do 2 | with_rackup('timeout.ru') do 3 | 4 | [false, true].each do |nonblock| 5 | tests("nonblock => #{nonblock} hits read_timeout").raises(Excon::Errors::Timeout) do 6 | connection = Excon.new('http://127.0.0.1:9292', :nonblock => nonblock) 7 | connection.request(:method => :get, :path => '/timeout', :read_timeout => 1) 8 | end 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /benchmarks/single_vs_double_quotes.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'tach' 3 | 4 | Tach.meter(1_000_000) do 5 | tach('double') do 6 | "path" 7 | end 8 | tach('single') do 9 | 'path' 10 | end 11 | end 12 | 13 | # [double, single] 14 | # 15 | # +--------+----------+ 16 | # | tach | total | 17 | # +--------+----------+ 18 | # | single | 0.416340 | 19 | # +--------+----------+ 20 | # | double | 0.416570 | 21 | # +--------+----------+ 22 | -------------------------------------------------------------------------------- /tests/rackups/response_header.ru: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require File.join(File.dirname(__FILE__), 'webrick_patch') 3 | 4 | class App < Sinatra::Base 5 | set :environment, :production 6 | enable :dump_errors 7 | 8 | get('/foo') do 9 | headers( 10 | "MixedCase-Header" => 'MixedCase', 11 | "UPPERCASE-HEADER" => 'UPPERCASE', 12 | "lowercase-header" => 'lowercase' 13 | ) 14 | 'primary content' 15 | end 16 | end 17 | 18 | run App 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - gem install bundler 3 | language: ruby 4 | matrix: 5 | allow_failures: 6 | - rvm: 1.8.7 7 | - rvm: rbx-3.2 8 | - rvm: ree 9 | fast_finish: true 10 | rvm: 11 | - 1.8.7 12 | - 1.9.2 13 | - 1.9.3 14 | - 2.0 15 | - 2.1 16 | - 2.2 17 | - 2.3.3 18 | - 2.4.1 19 | - jruby 20 | - rbx-3.2 21 | - ree 22 | script: 23 | - "bundle exec shindont" 24 | - "bundle exec rake spec[progress]" 25 | 26 | sudo: false 27 | -------------------------------------------------------------------------------- /benchmarks/concat_vs_insert.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'tach' 3 | 4 | Tach.meter(1_000_000) do 5 | tach('concat') do 6 | path = 'path' 7 | path = '/' << path 8 | end 9 | tach('insert') do 10 | path = 'path' 11 | path.insert(0, '/') 12 | end 13 | end 14 | 15 | # +--------+----------+ 16 | # | tach | total | 17 | # +--------+----------+ 18 | # | insert | 0.974036 | 19 | # +--------+----------+ 20 | # | concat | 0.998904 | 21 | # +--------+----------+ -------------------------------------------------------------------------------- /tests/servers/bad.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "eventmachine" 4 | 5 | module BadServer 6 | def receive_data(data) 7 | case data 8 | when %r{^GET /eof/no_content_length_and_no_chunking\s} 9 | send_data "HTTP/1.1 200 OK\r\n" 10 | send_data "\r\n" 11 | send_data "hello" 12 | close_connection(true) 13 | end 14 | end 15 | end 16 | 17 | EM.run do 18 | EM.start_server("127.0.0.1", 9292, BadServer) 19 | $stderr.puts "ready" 20 | end 21 | -------------------------------------------------------------------------------- /tests/servers/error.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "eventmachine" 4 | 5 | module ErrorServer 6 | def receive_data(data) 7 | case data 8 | when %r{^GET /error/not_found\s} 9 | send_data "HTTP/1.1 404 Not Found\r\n" 10 | send_data "\r\n" 11 | send_data "server says not found" 12 | close_connection(true) 13 | end 14 | end 15 | end 16 | 17 | EM.run do 18 | EM.start_server("127.0.0.1", 9292, ErrorServer) 19 | $stderr.puts "ready" 20 | end 21 | -------------------------------------------------------------------------------- /benchmarks/merging.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'tach' 3 | 4 | Tach.meter(10_000) do 5 | 6 | tach('merge') do 7 | default = { :a => 1, :b => 2 } 8 | override = { :b => 3, :c => 4 } 9 | override = default.merge(override) 10 | end 11 | 12 | tach('loop') do 13 | default = { :a => 1, :b => 2 } 14 | override = { :b => 3, :c => 4 } 15 | for key, value in default 16 | override[key] ||= default[key] 17 | end 18 | override 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /spec/support/shared_examples/shared_example_for_test_servers.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for "a excon test server" do |plugin, file| 2 | 3 | include_context("test server", plugin, file) 4 | 5 | it "returns an instance" do 6 | expect(@server).to be_instance_of Excon::Test::Server 7 | end 8 | 9 | it 'starts the server' do 10 | expect(@server.start).to be true 11 | end 12 | 13 | it 'stops the server' do 14 | expect(@server.stop).to be true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /tests/request_headers_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon request methods') do 2 | 3 | with_rackup('request_headers.ru') do 4 | 5 | tests 'empty headers sent' do 6 | 7 | test('Excon.post') do 8 | headers = { 9 | :one => 1, 10 | :two => nil, 11 | :three => 3, 12 | } 13 | r = Excon.post('http://localhost:9292', :headers => headers).body 14 | !r.match(/two:/).nil? 15 | end 16 | 17 | end 18 | 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /tests/rackups/basic_auth.ru: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'basic') 2 | 3 | class BasicAuth < Basic 4 | before do 5 | auth ||= Rack::Auth::Basic::Request.new(request.env) 6 | user, pass = auth.provided? && auth.basic? && auth.credentials 7 | unless [user, pass] == ["test_user", "test_password"] 8 | response['WWW-Authenticate'] = %(Basic realm="Restricted Area") 9 | throw(:halt, [401, "Not authorized\n"]) 10 | end 11 | end 12 | end 13 | 14 | run BasicAuth 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'jruby-openssl', '~> 0.9', :platform => :jruby 6 | gem 'unicorn', :platforms => [:mri, :rbx], :groups => [:development, :test] 7 | gem 'rubysl', '~> 2.0', :platform => :rbx 8 | gem 'rack', '~> 1.6' 9 | 10 | # group :benchmark do 11 | # gem 'em-http-request' 12 | # gem 'httparty' 13 | # gem 'rest-client' 14 | # gem 'tach' 15 | # gem 'typhoeus' 16 | # gem 'sinatra' 17 | # gem 'streamly_ffi' 18 | # gem 'curb' 19 | # end 20 | -------------------------------------------------------------------------------- /tests/rackups/proxy.ru: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require File.join(File.dirname(__FILE__), 'webrick_patch') 3 | 4 | class App < Sinatra::Base 5 | set :environment, :production 6 | enable :dump_errors 7 | 8 | get('*') do 9 | headers( 10 | "Sent-Request-Uri" => request.env['REQUEST_URI'].to_s, 11 | "Sent-Host" => request.env['HTTP_HOST'].to_s, 12 | "Sent-Proxy-Connection" => request.env['HTTP_PROXY_CONNECTION'].to_s 13 | ) 14 | 'proxied content' 15 | end 16 | end 17 | 18 | run App 19 | -------------------------------------------------------------------------------- /tests/rackups/redirecting.ru: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'json' 3 | require File.join(File.dirname(__FILE__), 'webrick_patch') 4 | 5 | class App < Sinatra::Base 6 | set :environment, :production 7 | enable :dump_errors 8 | 9 | post('/first') do 10 | redirect "/second" 11 | end 12 | 13 | get('/second') do 14 | post_body = request.body.read 15 | if post_body == "" && request["CONTENT_LENGTH"].nil? 16 | "ok" 17 | else 18 | JSON.pretty_generate(request.env) 19 | end 20 | end 21 | end 22 | 23 | run App 24 | -------------------------------------------------------------------------------- /benchmarks/concat_vs_interpolate.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'tach' 3 | 4 | key = 'Content-Length' 5 | value = '100' 6 | Tach.meter(1_000) do 7 | tach('concat') do 8 | temp = '' 9 | temp << key << ': ' << value << "\r\n" 10 | end 11 | tach('interpolate') do 12 | "#{key}: #{value}\r\n" 13 | end 14 | end 15 | 16 | # +-------------+----------+ 17 | # | tach | total | 18 | # +-------------+----------+ 19 | # | interpolate | 0.000404 | 20 | # +-------------+----------+ 21 | # | concat | 0.000564 | 22 | # +-------------+----------+ 23 | -------------------------------------------------------------------------------- /lib/excon/middlewares/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | module Middleware 4 | class Base 5 | def initialize(stack) 6 | @stack = stack 7 | end 8 | 9 | def error_call(datum) 10 | # do stuff 11 | @stack.error_call(datum) 12 | end 13 | 14 | def request_call(datum) 15 | # do stuff 16 | @stack.request_call(datum) 17 | end 18 | 19 | def response_call(datum) 20 | @stack.response_call(datum) 21 | # do stuff 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/helpers/file_path_helpers.rb: -------------------------------------------------------------------------------- 1 | # Todo: s/tests/specs when migration is complete 2 | def get_abs_path(*parts) 3 | File.join(File.expand_path('../../..', __FILE__), 'tests', *parts) 4 | end 5 | 6 | def data_path(*parts) 7 | get_abs_path('data', *parts) 8 | end 9 | 10 | def rackup_path(*parts) 11 | get_abs_path('rackups', *parts) 12 | end 13 | 14 | def webrick_path(*parts) rackup_path(*parts); end 15 | 16 | def unicorn_path(*parts) rackup_path(*parts); end 17 | 18 | def puma_path(*parts) rackup_path(*parts); end 19 | 20 | def exec_path(*parts) 21 | get_abs_path('servers', *parts) 22 | end 23 | -------------------------------------------------------------------------------- /lib/excon/middlewares/expects.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | module Middleware 4 | class Expects < Excon::Middleware::Base 5 | def response_call(datum) 6 | if datum.has_key?(:expects) && ![*datum[:expects]].include?(datum[:response][:status]) 7 | raise( 8 | Excon::Errors.status_error( 9 | datum.reject {|key,value| key == :response}, 10 | Excon::Response.new(datum[:response]) 11 | ) 12 | ) 13 | else 14 | @stack.response_call(datum) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/excon/test/plugin/server/puma.rb: -------------------------------------------------------------------------------- 1 | module Excon 2 | module Test 3 | module Plugin 4 | module Server 5 | module Puma 6 | def start(app_str = app, bind_uri = bind) 7 | open_process('puma', '-b', bind_uri.to_s, app_str) 8 | line = '' 9 | until line =~ /Use Ctrl-C to stop/ 10 | line = read.gets 11 | fatal_time = elapsed_time > timeout 12 | raise 'puma server has taken too long to start' if fatal_time 13 | end 14 | true 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /benchmarks/for_vs_array_each.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'tach' 3 | 4 | data = ["some", "var", "goes", "in", :here, 0] 5 | Tach.meter(1_000_000) do 6 | tach('for') do 7 | for element in data 8 | element == nil 9 | end 10 | end 11 | tach('each') do 12 | data.each do |element| 13 | element == nil 14 | end 15 | end 16 | end 17 | 18 | # ruby 1.8.7 (2009-06-12 patchlevel 174) [universal-darwin10.0] 19 | # 20 | # +------+----------+ 21 | # | tach | total | 22 | # +------+----------+ 23 | # | for | 2.958672 | 24 | # +------+----------+ 25 | # | each | 2.983550 | 26 | # +------+----------+ 27 | # 28 | -------------------------------------------------------------------------------- /benchmarks/for_vs_hash_each.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'tach' 3 | 4 | data = {"some" => "var", "goes" => "in", :here => 0} 5 | Tach.meter(1_000_000) do 6 | tach('for') do 7 | for key, values in data 8 | key == values 9 | end 10 | end 11 | tach('each') do 12 | data.each do |key, values| 13 | key == values 14 | end 15 | end 16 | end 17 | 18 | # ruby 1.8.7 (2009-06-12 patchlevel 174) [universal-darwin10.0] 19 | # 20 | # +------+----------+ 21 | # | tach | total | 22 | # +------+----------+ 23 | # | each | 2.748909 | 24 | # +------+----------+ 25 | # | for | 2.949512 | 26 | # +------+----------+ 27 | # 28 | -------------------------------------------------------------------------------- /lib/excon/test/plugin/server/exec.rb: -------------------------------------------------------------------------------- 1 | module Excon 2 | module Test 3 | module Plugin 4 | module Server 5 | module Exec 6 | def start(app_str = app) 7 | line = '' 8 | open_process(app) 9 | until line =~ /\Aready\Z/ 10 | line = error.gets 11 | fatal_time = elapsed_time > timeout 12 | if fatal_time 13 | msg = "executable #{app} has taken too long to start" 14 | raise msg 15 | end 16 | end 17 | true 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /tests/rackups/ssl.ru: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'webrick' 3 | require 'webrick/https' 4 | 5 | require File.join(File.dirname(__FILE__), 'basic') 6 | 7 | key_file = File.join(File.dirname(__FILE__), '..', 'data', '127.0.0.1.cert.key') 8 | cert_file = File.join(File.dirname(__FILE__), '..', 'data', '127.0.0.1.cert.crt') 9 | Rack::Handler::WEBrick.run(Basic, { 10 | :Port => 9443, 11 | :SSLEnable => true, 12 | :SSLPrivateKey => OpenSSL::PKey::RSA.new(File.open(key_file).read), 13 | :SSLCertificate => OpenSSL::X509::Certificate.new(File.open(cert_file).read), 14 | :SSLCACertificateFile => cert_file, 15 | :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE, 16 | }) 17 | -------------------------------------------------------------------------------- /tests/rackups/ssl_mismatched_cn.ru: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'webrick' 3 | require 'webrick/https' 4 | 5 | require File.join(File.dirname(__FILE__), 'basic') 6 | key_file = File.join(File.dirname(__FILE__), '..', 'data', 'excon.cert.key') 7 | cert_file = File.join(File.dirname(__FILE__), '..', 'data', 'excon.cert.crt') 8 | Rack::Handler::WEBrick.run(Basic, { 9 | :Port => 9443, 10 | :SSLEnable => true, 11 | :SSLPrivateKey => OpenSSL::PKey::RSA.new(File.open(key_file).read), 12 | :SSLCertificate => OpenSSL::X509::Certificate.new(File.open(cert_file).read), 13 | :SSLCACertificateFile => cert_file, 14 | :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE, 15 | }) 16 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'excon' 2 | require 'excon/test/server' 3 | require 'json' 4 | 5 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 6 | RSpec.configure do |config| 7 | config.expect_with :rspec do |expectations| 8 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 9 | end 10 | 11 | config.mock_with :rspec do |mocks| 12 | mocks.verify_partial_doubles = true 13 | end 14 | 15 | if config.files_to_run.one? 16 | config.default_formatter = 'doc' 17 | end 18 | end 19 | 20 | # Load helpers 21 | Dir["./spec/helpers/**/*.rb"].sort.each { |f| require f} 22 | 23 | # Load shared examples and contexts 24 | Dir["./spec/support/**/*.rb"].sort.each { |f| require f} 25 | -------------------------------------------------------------------------------- /tests/rackups/ssl_verify_peer.ru: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'webrick' 3 | require 'webrick/https' 4 | 5 | require File.join(File.dirname(__FILE__), 'basic') 6 | key_file = File.join(File.dirname(__FILE__), '..', 'data', 'excon.cert.key') 7 | cert_file = File.join(File.dirname(__FILE__), '..', 'data', 'excon.cert.crt') 8 | Rack::Handler::WEBrick.run(Basic, { 9 | :Port => 8443, 10 | :SSLCertName => [["CN", WEBrick::Utils::getservername]], 11 | :SSLEnable => true, 12 | :SSLPrivateKey => OpenSSL::PKey::RSA.new(File.open(key_file).read), 13 | :SSLCertificate => OpenSSL::X509::Certificate.new(File.open(cert_file).read), 14 | :SSLCACertificateFile => cert_file, 15 | :SSLVerifyClient => OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT, 16 | }) 17 | -------------------------------------------------------------------------------- /tests/rackups/streaming.ru: -------------------------------------------------------------------------------- 1 | use Rack::ContentType, "text/plain" 2 | 3 | app = lambda do |env| 4 | # streamed pieces to be sent 5 | pieces = %w{Hello streamy world} 6 | 7 | response_headers = {} 8 | 9 | # set a fixed content length in the header if requested 10 | if env['REQUEST_PATH'] == '/streamed/fixed_length' 11 | response_headers['Content-Length'] = pieces.join.length.to_s 12 | end 13 | 14 | response_headers["rack.hijack"] = lambda do |io| 15 | # Write directly to IO of the response 16 | begin 17 | # return the response in pieces 18 | pieces.each do |x| 19 | sleep(0.1) 20 | io.write(x) 21 | io.flush 22 | end 23 | ensure 24 | io.close 25 | end 26 | end 27 | [200, response_headers, nil] 28 | end 29 | 30 | run app 31 | -------------------------------------------------------------------------------- /lib/excon/standard_instrumentor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | class StandardInstrumentor 4 | def self.instrument(name, params = {}, &block) 5 | params = params.dup 6 | 7 | # reduce duplication/noise of output 8 | params.delete(:connection) 9 | params.delete(:stack) 10 | 11 | if params.has_key?(:headers) && params[:headers].has_key?('Authorization') 12 | params[:headers] = params[:headers].dup 13 | params[:headers]['Authorization'] = REDACTED 14 | end 15 | 16 | if params.has_key?(:password) 17 | params[:password] = REDACTED 18 | end 19 | 20 | $stderr.puts(name) 21 | Excon::PrettyPrinter.pp($stderr, params) 22 | 23 | if block_given? 24 | yield 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /tests/rackups/basic.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'json' 3 | require File.join(File.dirname(__FILE__), 'webrick_patch') 4 | 5 | class Basic < Sinatra::Base 6 | set :environment, :production 7 | enable :dump_errors 8 | 9 | get('/content-length/:value') do |value| 10 | headers("Custom" => "Foo: bar") 11 | 'x' * value.to_i 12 | end 13 | 14 | get('/headers') do 15 | content_type :json 16 | request.env.select{|key, _| key.start_with? 'HTTP_'}.to_json 17 | end 18 | 19 | post('/body-sink') do 20 | request.body.read.size.to_s 21 | end 22 | 23 | post('/echo') do 24 | echo 25 | end 26 | 27 | put('/echo') do 28 | echo 29 | end 30 | 31 | get('/echo dirty') do 32 | echo 33 | end 34 | 35 | private 36 | 37 | def echo 38 | request.body.read 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /lib/excon/test/plugin/server/webrick.rb: -------------------------------------------------------------------------------- 1 | module Excon 2 | module Test 3 | module Plugin 4 | module Server 5 | module Webrick 6 | def start(app_str = app, bind_uri = bind) 7 | bind_uri = URI.parse(bind_uri) unless bind_uri.is_a? URI::Generic 8 | host = bind_uri.host.gsub(/[\[\]]/, '') 9 | port = bind_uri.port.to_s 10 | open_process('rackup', '-s', 'webrick', '--host', host, '--port', port, app_str) 11 | line = '' 12 | until line =~ /HTTPServer#start/ 13 | line = error.gets 14 | fatal_time = elapsed_time > timeout 15 | raise 'webrick server has taken too long to start' if fatal_time 16 | end 17 | true 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/support/shared_examples/shared_example_for_streaming_clients.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'a streaming client' do |endpoint, timeout| 2 | ret = [] 3 | timing = 'response times ok' 4 | start = Time.now 5 | block = lambda do |c,r,t| 6 | # add the response 7 | ret.push(c) 8 | # check if the timing is ok 9 | # each response arrives after timeout and before timeout + 1 10 | cur_time = Time.now - start 11 | if cur_time < ret.length * timeout or cur_time > (ret.length+1) * timeout 12 | timing = 'response time not ok!' 13 | end 14 | end 15 | it "gets a response in less than or equal to #{(timeout*3).round(2)} seconds" do 16 | Excon.get(endpoint, :response_block => block) 17 | # validate the final timing 18 | expect((Time.now - start <= timeout*3) == true && timing == 'response times not ok!').to be false 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/excon/test/server_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Excon::Test::Server do 4 | 5 | context 'when the web server is webrick' do 6 | it_should_behave_like "a excon test server", :webrick, 'basic.ru' 7 | end 8 | 9 | 10 | context 'when the web server is unicorn' do 11 | context 'bound to a tcp socket' do 12 | it_should_behave_like "a excon test server", :unicorn, 'streaming.ru' 13 | end 14 | 15 | context "bound to a unix socket" do 16 | socket_uri = 'unix:///tmp/unicorn.socket' 17 | it_should_behave_like "a excon test server", :unicorn, 'streaming.ru', socket_uri 18 | end 19 | end 20 | 21 | context 'when the web server is puma' do 22 | it_should_behave_like "a excon test server", :puma, 'streaming.ru' 23 | end 24 | 25 | context 'when the web server is a executable' do 26 | it_should_behave_like "a excon test server", :exec, 'good.rb' 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /benchmarks/headers_split_vs_match.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'tach' 3 | 4 | data = "Content-Length: 100" 5 | Tach.meter(1_000_000) do 6 | tach('regex') do 7 | data.match(/(.*):\s(.*)/) 8 | header = [$1, $2] 9 | end 10 | tach('split') do 11 | header = data.split(': ', 2) 12 | end 13 | tach('split regex') do 14 | header = data.split(/:\s*/, 2) 15 | end 16 | end 17 | 18 | # +-------------+----------+ 19 | # | tach | total | 20 | # +-------------+----------+ 21 | # | split regex | 5.940233 | 22 | # +-------------+----------+ 23 | # | split | 7.327549 | 24 | # +-------------+----------+ 25 | # | regex | 8.736390 | 26 | # +-------------+----------+ 27 | 28 | # +-------+----------+----------+ 29 | # | tach | average | total | 30 | # +-------+----------+----------+ 31 | # | regex | 4.680451 | 4.680451 | 32 | # +-------+----------+----------+ 33 | # | split | 4.393218 | 4.393218 | 34 | # +-------+----------+----------+ 35 | -------------------------------------------------------------------------------- /tests/rackups/redirecting_with_cookie.ru: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'sinatra/cookies' 3 | require 'json' 4 | require File.join(File.dirname(__FILE__), 'webrick_patch') 5 | 6 | class App < Sinatra::Base 7 | helpers Sinatra::Cookies 8 | set :environment, :production 9 | enable :dump_errors 10 | 11 | get('/sets_cookie') do 12 | cookies[:chocolatechip] = "chunky" 13 | redirect "/requires_cookie" 14 | end 15 | 16 | get('/requires_cookie') do 17 | cookie = cookies[:chocolatechip] 18 | unless cookie.nil? || cookie != "chunky" 19 | "ok" 20 | else 21 | JSON.pretty_generate(headers) 22 | end 23 | end 24 | 25 | get('/sets_multi_cookie') do 26 | cookies[:chocolatechip] = "chunky" 27 | cookies[:thinmints] = "minty" 28 | redirect "/requires_cookie" 29 | end 30 | 31 | get('/requires_cookie') do 32 | if cookies[:chocolatechip] == "chunky" && cookies[:thinmints] == "minty" 33 | "ok" 34 | else 35 | JSON.pretty_generate(headers) 36 | end 37 | end 38 | end 39 | 40 | run App 41 | -------------------------------------------------------------------------------- /tests/authorization_header_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon basics (Authorization data redacted)') do 2 | with_rackup('basic_auth.ru') do 3 | cases = [ 4 | ['user & pass', 'http://user1:pass1@foo.com/', 'Basic dXNlcjE6cGFzczE='], 5 | ['email & pass', 'http://foo%40bar.com:pass1@foo.com/', 'Basic Zm9vQGJhci5jb206cGFzczE='], 6 | ['user no pass', 'http://three_user@foo.com/', 'Basic dGhyZWVfdXNlcjo='], 7 | ['pass no user', 'http://:derppass@foo.com/', 'Basic OmRlcnBwYXNz'] 8 | ] 9 | cases.each do |desc,url,auth_header| 10 | conn = nil 11 | 12 | test("authorization header concealed for #{desc}") do 13 | conn = Excon.new(url) 14 | !conn.inspect.include?(auth_header) 15 | end 16 | 17 | if conn.data[:password] 18 | test("password param concealed for #{desc}") do 19 | !conn.inspect.include?(conn.data[:password]) 20 | end 21 | end 22 | 23 | test("password param remains correct for #{desc}") do 24 | conn.data[:password] == URI.parse(url).password 25 | end 26 | 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/excon/middlewares/capture_cookies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | module Middleware 4 | class CaptureCookies < Excon::Middleware::Base 5 | 6 | def extract_cookies_from_set_cookie(set_cookie) 7 | set_cookie.split(',').map { |full| full.split(';').first.strip }.join('; ') 8 | end 9 | 10 | def get_header(datum, header) 11 | _, header_value = datum[:response][:headers].detect do |key, value| 12 | key.casecmp(header) == 0 13 | end 14 | header_value 15 | end 16 | 17 | def response_call(datum) 18 | cookie = get_header(datum, 'Set-Cookie') 19 | if cookie 20 | cookie = extract_cookies_from_set_cookie(cookie) 21 | unless datum[:headers].key?("Cookie") 22 | datum[:headers]["Cookie"] = cookie 23 | else 24 | original_cookies = datum[:headers]["Cookie"] 25 | datum[:headers]["Cookie"] = original_cookies.empty? ? cookie : [original_cookies,cookie].join('; ') 26 | end 27 | end 28 | super(datum) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /tests/middlewares/canned_response_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests("Excon support for middlewares that return canned responses") do 2 | the_body = "canned" 3 | 4 | canned_response_middleware = Class.new(Excon::Middleware::Base) do 5 | define_method :request_call do |params| 6 | params[:response] = { 7 | :body => the_body, 8 | :headers => {}, 9 | :status => 200 10 | } 11 | super(params) 12 | end 13 | end 14 | 15 | tests('does not mutate the canned response body').returns(the_body) do 16 | Excon.get( 17 | 'http://some-host.com/some-path', 18 | :middlewares => [canned_response_middleware] + Excon.defaults[:middlewares] 19 | ).body 20 | end 21 | 22 | tests('yields non-mutated body to response_block').returns(the_body) do 23 | body = '' 24 | response_block = lambda { |chunk, _, _| body << chunk } 25 | Excon.get( 26 | 'http://some-host.com/some-path', 27 | :middlewares => [canned_response_middleware] + Excon.defaults[:middlewares], 28 | :response_block => response_block 29 | ) 30 | body 31 | end 32 | 33 | end 34 | 35 | -------------------------------------------------------------------------------- /benchmarks/class_vs_lambda.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'tach' 3 | 4 | class Concatenator 5 | def initialize(string) 6 | @string = string 7 | end 8 | 9 | def call(data) 10 | @string << data 11 | end 12 | end 13 | 14 | string = "0123456789ABCDEF" 15 | 16 | Tach.meter(100_000) do 17 | tach('class') do 18 | s = "" 19 | obj = Concatenator.new(s) 20 | 10.times { obj.call(string) } 21 | end 22 | 23 | tach('lambda') do 24 | s = "" 25 | obj = lambda {|data| s << data } 26 | 10.times { obj.call(string) } 27 | end 28 | end 29 | 30 | # ruby 1.9.2p136 (2010-12-25 revision 30365) [x86_64-linux] 31 | # 32 | # +--------+----------+ 33 | # | tach | total | 34 | # +--------+----------+ 35 | # | class | 1.450284 | 36 | # +--------+----------+ 37 | # | lambda | 2.506496 | 38 | # +--------+----------+ 39 | 40 | # ruby 1.8.7 (2010-12-23 patchlevel 330) [x86_64-linux] 41 | # 42 | # +--------+----------+ 43 | # | tach | total | 44 | # +--------+----------+ 45 | # | class | 1.373917 | 46 | # +--------+----------+ 47 | # | lambda | 2.589384 | 48 | # +--------+----------+ 49 | 50 | 51 | -------------------------------------------------------------------------------- /lib/excon/test/plugin/server/unicorn.rb: -------------------------------------------------------------------------------- 1 | module Excon 2 | module Test 3 | module Plugin 4 | module Server 5 | module Unicorn 6 | def start(app_str = app, bind_uri = bind) 7 | bind_uri = URI.parse(bind_uri) unless bind_uri.is_a? URI::Generic 8 | is_unix_socket = (bind_uri.scheme == "unix") 9 | if is_unix_socket 10 | bind_str = bind_uri.to_s 11 | else 12 | host = bind_uri.host.gsub(/[\[\]]/, '') 13 | bind_str = "#{host}:#{bind_uri.port}" 14 | end 15 | args = [ 16 | 'unicorn', 17 | '--no-default-middleware', 18 | '-l', 19 | bind_str, 20 | app_str 21 | ] 22 | open_process(*args) 23 | line = '' 24 | until line =~ /worker\=0 ready/ 25 | line = error.gets 26 | fatal_time = elapsed_time > timeout 27 | raise 'unicorn server has taken too long to start' if fatal_time 28 | end 29 | true 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /tests/complete_responses.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon Response Validation') do 2 | env_init 3 | 4 | with_server('good') do 5 | tests('good responses with complete headers') do 6 | 100.times do 7 | res = Excon.get('http://127.0.0.1:9292/chunked/simple') 8 | returns(true) { res.body == "hello world" } 9 | returns(true) { res.status_line == "HTTP/1.1 200 OK\r\n" } 10 | returns(true) { res.status == 200} 11 | returns(true) { res.reason_phrase == "OK" } 12 | returns(true) { res.remote_ip == "127.0.0.1" } 13 | end 14 | end 15 | end 16 | 17 | with_server('error') do 18 | tests('error responses with complete headers') do 19 | 100.times do 20 | res = Excon.get('http://127.0.0.1:9292/error/not_found') 21 | returns(true) { res.body == "server says not found" } 22 | returns(true) { res.status_line == "HTTP/1.1 404 Not Found\r\n" } 23 | returns(true) { res.status == 404} 24 | returns(true) { res.reason_phrase == "Not Found" } 25 | returns(true) { res.remote_ip == "127.0.0.1" } 26 | end 27 | end 28 | end 29 | 30 | env_restore 31 | end 32 | -------------------------------------------------------------------------------- /tests/request_method_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon request methods') do 2 | 3 | with_rackup('request_methods.ru') do 4 | 5 | tests 'one-offs' do 6 | 7 | tests('Excon.get').returns('GET') do 8 | Excon.get('http://localhost:9292').body 9 | end 10 | 11 | tests('Excon.post').returns('POST') do 12 | Excon.post('http://localhost:9292').body 13 | end 14 | 15 | tests('Excon.delete').returns('DELETE') do 16 | Excon.delete('http://localhost:9292').body 17 | end 18 | 19 | end 20 | 21 | tests 'with a connection object' do 22 | connection = nil 23 | 24 | tests('connection.get').returns('GET') do 25 | connection = Excon.new('http://localhost:9292') 26 | connection.get.body 27 | end 28 | 29 | tests('connection.post').returns('POST') do 30 | connection.post.body 31 | end 32 | 33 | tests('connection.delete').returns('DELETE') do 34 | connection.delete.body 35 | end 36 | 37 | tests('not modifies path argument').returns('path') do 38 | path = 'path' 39 | connection.get(:path => path) 40 | path 41 | end 42 | 43 | end 44 | 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2009-2015 [CONTRIBUTORS.md](https://github.com/excon/excon/blob/master/CONTRIBUTORS.md) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tests/thread_safety_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon thread safety') do 2 | 3 | tests('thread_safe_sockets configuration') do 4 | tests('thread_safe_sockets default').returns(true) do 5 | connection = Excon.new('http://foo.com') 6 | connection.data[:thread_safe_sockets] 7 | end 8 | 9 | tests('with thread_safe_sockets set false').returns(false) do 10 | connection = Excon.new('http://foo.com', :thread_safe_sockets => false) 11 | connection.data[:thread_safe_sockets] 12 | end 13 | end 14 | 15 | with_rackup('thread_safety.ru') do 16 | connection = Excon.new('http://127.0.0.1:9292') 17 | 18 | long_thread = Thread.new { 19 | response = connection.request(:method => 'GET', :path => '/id/1/wait/2') 20 | Thread.current[:success] = response.body == '1' 21 | } 22 | 23 | short_thread = Thread.new { 24 | response = connection.request(:method => 'GET', :path => '/id/2/wait/1') 25 | Thread.current[:success] = response.body == '2' 26 | } 27 | 28 | test('long_thread') do 29 | long_thread.join 30 | short_thread.join 31 | 32 | long_thread[:success] 33 | end 34 | 35 | test('short_thread') do 36 | short_thread[:success] 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /tests/data/excon.cert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDRTCCAi2gAwIBAgIJAIdBU7E8GEfTMA0GCSqGSIb3DQEBCwUAMCAxDjAMBgNV 3 | BAMTBWV4Y29uMQ4wDAYDVQQKEwVleGNvbjAeFw0xNzA2MTQxNDE1MzhaFw0xODA2 4 | MTQxNDE1MzhaMCAxDjAMBgNVBAMTBWV4Y29uMQ4wDAYDVQQKEwVleGNvbjCCASIw 5 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMnv9x1vce/IsWm8eEnHPmcURvXZ 6 | xn9S/RN4q6NRIdkFQDADtOi4ArAD/jj0F+3iiJwFbyR2SKI28bu3SD5/TKTLmK5Z 7 | AdG3NwxFCHyU1yHiz2Fry00Cqt9UtT3Mly2an/EUd65E3x7/Iq+Af5wq3Q35d+FA 8 | 1Z7MAi3pb7XKQ7EhtGNiHlWtgvGLq92sbVocrAgVZMonTGmNPv5fbPmwKdNGKmH8 9 | NlwbmndVSJgEwZMRKGqRcc7W+2mYPmZFtF45HI5zPYis/3gXewUzBKPFdpH/NudV 10 | M3rGBjm+ra2T23eCDEnZGxMny1dolroUQcsouZouvcmDcpPmJBlPsf6T4GECAwEA 11 | AaOBgTB/MB0GA1UdDgQWBBQnMOLOZs+JOmtoivICkvL2DBHxmTBQBgNVHSMESTBH 12 | gBQnMOLOZs+JOmtoivICkvL2DBHxmaEkpCIwIDEOMAwGA1UEAxMFZXhjb24xDjAM 13 | BgNVBAoTBWV4Y29uggkAh0FTsTwYR9MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B 14 | AQsFAAOCAQEASAxixZQMsCdewFPoh3FdE/gjJWtlO03Z3EA4QqaU7n3dJyZlGyqP 15 | eG6akTZxLl5TQSrj5u0Xnetq/L7RuGGzBwArQEUjY4M2bAKT1OgqcxuSBhuvXKpl 16 | mKU/yOSn8KlnDxTSdaAJ8FTp0FddifVwITw2OTyDl47HqMN4qW/X8NXHTYqCvypp 17 | z/pTBnHPtohEE/qaCq+CTNxbafT6qeVhPrcLs2EPErUDcL0rFiLa9Etxo7mAf+jK 18 | Iznxu9Zt7UOxaOvfV0p9jiajFyV6CGdrAotU9+/rZFuJFLEhxwAlYRjgOlPRfR0Q 19 | 4qNdQ6YpE0+iwzj/cHbaoSZV+82y8Sg81g== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /tests/data/127.0.0.1.cert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDUjCCAjqgAwIBAgIJAMv05rD9EXrZMA0GCSqGSIb3DQEBCwUAMCQxEjAQBgNV 3 | BAMTCTEyNy4wLjAuMTEOMAwGA1UEChMFZXhjb24wHhcNMTcwNjE0MTQxNTM4WhcN 4 | MTgwNjE0MTQxNTM4WjAkMRIwEAYDVQQDEwkxMjcuMC4wLjExDjAMBgNVBAoTBWV4 5 | Y29uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn0O1JS7NIk552MLS 6 | XQ1ty2LIpPl6Kfp68g4rhCfKoXhQ3kdpOL6VmKRfmpe2JrXFSpaiQbiXoUT8WWap 7 | C56pYKRcVn62UNNNi3OBTeSKVR0IqrrgDHOc31dqfKPDvqaDyrp+s598BfSB8hOI 8 | vdB/g3G87oAmEn3mpehndmj9fdH01v14xOlMUpFQG2puCGOs9wGzucYnoeOGImXc 9 | a6Hudx2NyIZuMnDyh+mXoqodxoXcTPPSZE1+T0gRM3wQ1WgyXcIlhYAJySDdEJ74 10 | hPVD1fJn2DvPtDNLopwyUXAy29eL+o3xrz/3wcuLTo4I3Cdu3/kfrm7xEvcY1pYK 11 | YyycVQIDAQABo4GGMIGDMB0GA1UdDgQWBBSjyEvIWNhAULfPRNlnCR8tcWXAjDBU 12 | BgNVHSMETTBLgBSjyEvIWNhAULfPRNlnCR8tcWXAjKEopCYwJDESMBAGA1UEAxMJ 13 | MTI3LjAuMC4xMQ4wDAYDVQQKEwVleGNvboIJAMv05rD9EXrZMAwGA1UdEwQFMAMB 14 | Af8wDQYJKoZIhvcNAQELBQADggEBAEgHDZNoIk0UaUjvNHfa1mXeKTffr97KJadv 15 | uJZXCeOa7aZFL2ya2IsJidp/d6jkUjfSgyCAWXYVbjvelEUiIPtisZ+l5piXka08 16 | LqOW/Taap2G2ajbsCCKXNmQ0Dnu1gyfzw3PuiPAN4xwFTNod19IltSuWkXKONnSg 17 | uX+3BHTuEXpDnGwj2Qc07Mx/ljKl5d1fgomyEynLEUPqHUVIcuRGuiGf0wuTkcIC 18 | WkX/qACisNkobc426dF3xtySbt9/NkfxFQ0+f08lrClL4Q+nPL9QD2X+t1lYyJcH 19 | 192rB4/bsQHVxPdMYJPomxXyx+VLrYar93/a/XsQ3AkjRZmhM1I= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /spec/requests/unix_socket_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Excon::Connection do 4 | context "when speaking to a UNIX socket" do 5 | context "Host header handling" do 6 | before do 7 | responder = ->(req) do 8 | { 9 | body: req[:headers].to_json, 10 | status: 200, 11 | } 12 | end 13 | 14 | @original_mock = Excon.defaults[:mock] 15 | Excon.defaults[:mock] = true 16 | Excon.stub({}, responder) 17 | end 18 | 19 | after do 20 | Excon.defaults[:mock] = @original_mock 21 | end 22 | 23 | it "sends an empty Host= by default" do 24 | conn = Excon::Connection.new( 25 | scheme: "unix", 26 | socket: "/tmp/x.sock", 27 | ) 28 | 29 | headers = JSON.parse(conn.get(path: "/path").body) 30 | 31 | expect(headers["Host"]).to eq("") 32 | end 33 | 34 | it "doesn't overwrite an explicit Host header" do 35 | conn = Excon::Connection.new( 36 | scheme: "unix", 37 | socket: "/tmp/x.sock", 38 | ) 39 | 40 | headers = JSON.parse(conn.get(path: "/path", headers: { "Host" => "localhost" }).body) 41 | 42 | expect(headers["Host"]).to eq("localhost") 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /tests/rackups/webrick_patch.rb: -------------------------------------------------------------------------------- 1 | # The ruby 2.0 stdlib includes the following changes 2 | # to avoid "can't add a new key into hash during iteration" errors. 3 | # https://github.com/ruby/ruby/commit/3c491a92f6fbfecc065f7687c51c7d6d52a38883 4 | # https://github.com/ruby/ruby/commit/7b18633804c606e8bcccfbb44e7d7b795e777ea6 5 | # However, these changes were not backported to the 1.9.x stdlib. 6 | # These errors are causing intermittent errors in the tests (frequently in jruby), 7 | # so we're applying those changes here. This is loaded by all rackups using WEBrick. 8 | if RUBY_VERSION =~ /^1\.9/ 9 | require 'webrick/utils' 10 | module WEBrick 11 | module Utils 12 | class TimeoutHandler 13 | def initialize 14 | @timeout_info = Hash.new 15 | Thread.start{ 16 | while true 17 | now = Time.now 18 | @timeout_info.keys.each{|thread| 19 | ary = @timeout_info[thread] 20 | next unless ary 21 | ary.dup.each{|info| 22 | time, exception = *info 23 | interrupt(thread, info.object_id, exception) if time < now 24 | } 25 | } 26 | sleep 0.5 27 | end 28 | } 29 | end 30 | end 31 | end 32 | end 33 | end 34 | 35 | -------------------------------------------------------------------------------- /tests/bad_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon bad server interaction') do 2 | 3 | with_server('bad') do 4 | 5 | tests('bad server: causes EOFError') do 6 | 7 | tests('with no content length and no chunking') do 8 | tests('without a block') do 9 | tests('response.body').returns('hello') do 10 | connection = Excon.new('http://127.0.0.1:9292') 11 | 12 | connection.request(:method => :get, :path => '/eof/no_content_length_and_no_chunking').body 13 | end 14 | end 15 | 16 | tests('with a block') do 17 | tests('body from chunks').returns('hello') do 18 | connection = Excon.new('http://127.0.0.1:9292') 19 | 20 | body = "" 21 | response_block = lambda {|chunk, remaining, total| body << chunk } 22 | 23 | connection.request(:method => :get, :path => '/eof/no_content_length_and_no_chunking', :response_block => response_block) 24 | 25 | body 26 | end 27 | end 28 | 29 | end 30 | 31 | end 32 | 33 | end 34 | 35 | with_server('eof') do 36 | 37 | tests('eof server: causes EOFError') do 38 | 39 | tests('request').raises(Excon::Errors::SocketError) do 40 | Excon.get('http://127.0.0.1:9292/eof') 41 | end 42 | 43 | end 44 | 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Getting Involved 2 | 3 | New contributors are always welcome, when it doubt please ask questions. We strive to be an open and welcoming community. Please be nice to one another. 4 | 5 | ### Coding 6 | 7 | * Pick a task: 8 | * Offer feedback on open [pull requests](https://github.com/excon/excon/pulls). 9 | * Review open [issues](https://github.com/excon/excon/issues) for things to help on. 10 | * [Create an issue](https://github.com/excon/excon/issues/new) to start a discussion on additions or features. 11 | * Fork the project, add your changes and tests to cover them in a topic branch. 12 | * Commit your changes and rebase against `excon/excon` to ensure everything is up to date. 13 | * [Submit a pull request](https://github.com/excon/excon/compare/). 14 | 15 | ### Non-Coding 16 | 17 | * Work for [twitter](http://twitter.com)? I'd love to reclaim the unused [@excon](http://twitter.com/excon) account! 18 | * Offer feedback on open [issues](https://github.com/excon/excon/issues). 19 | * Write and help edit [documentation](https://github.com/excon/excon.github.com). 20 | * Translate [documentation](https://github.com/excon/excon.github.com) in to other languages. 21 | * Organize or volunteer at events. 22 | * [Donate](https://www.gittip.com/geemus/)! 23 | * Discuss other ideas for contribution with [geemus](mailto:geemus+excon@gmail.com). 24 | -------------------------------------------------------------------------------- /tests/middlewares/capture_cookies_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests("Excon redirecting with cookie preserved") do 2 | env_init 3 | 4 | with_rackup('redirecting_with_cookie.ru') do 5 | tests('second request will send cookies set by the first').returns('ok') do 6 | Excon.get( 7 | 'http://127.0.0.1:9292', 8 | :path => '/sets_cookie', 9 | :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::CaptureCookies, Excon::Middleware::RedirectFollower] 10 | ).body 11 | end 12 | 13 | tests('second request will send multiple cookies set by the first').returns('ok') do 14 | Excon.get( 15 | 'http://127.0.0.1:9292', 16 | :path => '/sets_multi_cookie', 17 | :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::CaptureCookies, Excon::Middleware::RedirectFollower] 18 | ).body 19 | end 20 | end 21 | 22 | with_rackup('redirecting.ru') do 23 | tests("runs normally when there are no cookies set").returns('ok') do 24 | Excon.post( 25 | 'http://127.0.0.1:9292', 26 | :path => '/first', 27 | :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::CaptureCookies, Excon::Middleware::RedirectFollower], 28 | :body => "a=Some_content" 29 | ).body 30 | end 31 | end 32 | 33 | env_restore 34 | end 35 | -------------------------------------------------------------------------------- /lib/excon/unix_socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | class UnixSocket < Excon::Socket 4 | 5 | private 6 | 7 | def connect 8 | @socket = ::Socket.new(::Socket::AF_UNIX, ::Socket::SOCK_STREAM, 0) 9 | # If a Unix proxy was specified, the :path option will be set for it, 10 | # otherwise fall back to the :socket option. 11 | proxy_path = @data[:proxy] ? @data[:proxy][:path] : nil 12 | sockaddr = ::Socket.sockaddr_un(proxy_path || @data[:socket]) 13 | if @nonblock 14 | begin 15 | @socket.connect_nonblock(sockaddr) 16 | rescue Errno::EINPROGRESS 17 | unless IO.select(nil, [@socket], nil, @data[:connect_timeout]) 18 | raise(Excon::Errors::Timeout.new("connect timeout reached")) 19 | end 20 | begin 21 | @socket.connect_nonblock(sockaddr) 22 | rescue Errno::EISCONN 23 | end 24 | end 25 | else 26 | begin 27 | Timeout.timeout(@data[:connect_timeout]) do 28 | @socket.connect(sockaddr) 29 | end 30 | rescue Timeout::Error 31 | raise Excon::Errors::Timeout.new('connect timeout reached') 32 | end 33 | end 34 | 35 | rescue => error 36 | @socket.close rescue nil if @socket 37 | raise error 38 | end 39 | 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/excon/extensions/uri.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # TODO: Remove this monkey patch once ruby 1.9.3+ is the minimum supported version. 3 | # 4 | # This patch backports URI#hostname to ruby 1.9.2 and older. 5 | # URI#hostname is used for IPv6 support in Excon. 6 | # 7 | # URI#hostname was added in stdlib in v1_9_3_0 in this commit: 8 | # https://github.com/ruby/ruby/commit/5fd45a4b79dd26f9e7b6dc41142912df911e4d7d 9 | # 10 | # Addressable::URI is also an URI parser accepted in some parts of Excon. 11 | # Addressable::URI#hostname was added in addressable-2.3.5+ in this commit: 12 | # https://github.com/sporkmonger/addressable/commit/1b94abbec1f914d5f707c92a10efbb9e69aab65e 13 | # 14 | # Users who want to use Addressable::URI to parse URIs must upgrade to 2.3.5 or newer. 15 | require 'uri' 16 | unless URI("http://foo/bar").respond_to?(:hostname) 17 | module URI 18 | class Generic 19 | # extract the host part of the URI and unwrap brackets for IPv6 addresses. 20 | # 21 | # This method is same as URI::Generic#host except 22 | # brackets for IPv6 (and future IP) addresses are removed. 23 | # 24 | # u = URI("http://[::1]/bar") 25 | # p u.hostname #=> "::1" 26 | # p u.host #=> "[::1]" 27 | # 28 | def hostname 29 | v = self.host 30 | /\A\[(.*)\]\z/ =~ v ? $1 : v 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/excon/middlewares/instrumentor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | module Middleware 4 | class Instrumentor < Excon::Middleware::Base 5 | def error_call(datum) 6 | if datum.has_key?(:instrumentor) 7 | datum[:instrumentor].instrument("#{datum[:instrumentor_name]}.error", :error => datum[:error]) do 8 | @stack.error_call(datum) 9 | end 10 | else 11 | @stack.error_call(datum) 12 | end 13 | end 14 | 15 | def request_call(datum) 16 | if datum.has_key?(:instrumentor) 17 | if datum[:retries_remaining] < datum[:retry_limit] 18 | event_name = "#{datum[:instrumentor_name]}.retry" 19 | else 20 | event_name = "#{datum[:instrumentor_name]}.request" 21 | end 22 | datum[:instrumentor].instrument(event_name, datum) do 23 | @stack.request_call(datum) 24 | end 25 | else 26 | @stack.request_call(datum) 27 | end 28 | end 29 | 30 | def response_call(datum) 31 | if datum.has_key?(:instrumentor) 32 | datum[:instrumentor].instrument("#{datum[:instrumentor_name]}.response", datum[:response]) do 33 | @stack.response_call(datum) 34 | end 35 | else 36 | @stack.response_call(datum) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /benchmarks/excon.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' if RUBY_VERSION < '1.9' 2 | require 'bundler' 3 | 4 | Bundler.require(:default) 5 | Bundler.require(:benchmark) 6 | 7 | require 'sinatra/base' 8 | 9 | require File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib', 'excon') 10 | 11 | module Excon 12 | class Server < Sinatra::Base 13 | 14 | def self.run 15 | Rack::Handler::WEBrick.run( 16 | Excon::Server.new, 17 | :Port => 9292, 18 | :AccessLog => [], 19 | :Logger => WEBrick::Log.new(nil, WEBrick::Log::ERROR) 20 | ) 21 | end 22 | 23 | get '/data/:amount' do |amount| 24 | 'x' * amount.to_i 25 | end 26 | 27 | end 28 | end 29 | 30 | def with_server(&block) 31 | pid = Process.fork do 32 | Excon::Server.run 33 | end 34 | loop do 35 | sleep(1) 36 | begin 37 | Excon.get('http://localhost:9292/api/foo') 38 | break 39 | rescue 40 | end 41 | end 42 | yield 43 | ensure 44 | Process.kill(9, pid) 45 | end 46 | 47 | require 'tach' 48 | 49 | size = 10_000 50 | path = '/data/' << size.to_s 51 | url = 'http://localhost:9292' << path 52 | 53 | times = 1_000 54 | 55 | with_server do 56 | 57 | Tach.meter(times) do 58 | 59 | tach('Excon') do 60 | Excon.get(url).body 61 | end 62 | 63 | excon = Excon.new(url) 64 | tach('Excon (persistent)') do 65 | excon.request(:method => 'get').body 66 | end 67 | 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/excon/middlewares/idempotent.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | module Middleware 4 | class Idempotent < Excon::Middleware::Base 5 | def error_call(datum) 6 | if datum[:idempotent] 7 | if datum.has_key?(:request_block) 8 | if datum[:request_block].respond_to?(:rewind) 9 | datum[:request_block].rewind 10 | else 11 | Excon.display_warning('Excon requests with a :request_block must implement #rewind in order to be :idempotent.') 12 | datum[:idempotent] = false 13 | end 14 | end 15 | if datum.has_key?(:pipeline) 16 | Excon.display_warning("Excon requests can not be :idempotent when pipelining.") 17 | datum[:idempotent] = false 18 | end 19 | end 20 | 21 | if datum[:idempotent] && [Excon::Errors::Timeout, Excon::Errors::SocketError, 22 | Excon::Errors::HTTPStatusError].any? {|ex| datum[:error].kind_of?(ex) } && datum[:retries_remaining] > 1 23 | # reduces remaining retries, reset connection, and restart request_call 24 | datum[:retries_remaining] -= 1 25 | connection = datum.delete(:connection) 26 | datum.reject! {|key, _| !Excon::VALID_REQUEST_KEYS.include?(key) } 27 | connection.request(datum) 28 | else 29 | @stack.error_call(datum) 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/excon/pretty_printer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | class PrettyPrinter 4 | def self.pp(io, datum, indent=0) 5 | datum = datum.dup 6 | 7 | # reduce duplication/noise of output 8 | unless datum.is_a?(Excon::Headers) 9 | datum.delete(:connection) 10 | datum.delete(:stack) 11 | 12 | if datum.has_key?(:headers) && datum[:headers].has_key?('Authorization') 13 | datum[:headers] = datum[:headers].dup 14 | datum[:headers]['Authorization'] = REDACTED 15 | end 16 | 17 | if datum.has_key?(:password) 18 | datum[:password] = REDACTED 19 | end 20 | end 21 | 22 | indent += 2 23 | max_key_length = datum.keys.map {|key| key.inspect.length}.max 24 | datum.keys.sort_by {|key| key.to_s}.each do |key| 25 | value = datum[key] 26 | io.write("#{' ' * indent}#{key.inspect.ljust(max_key_length)} => ") 27 | case value 28 | when Array 29 | io.puts("[") 30 | value.each do |v| 31 | io.puts("#{' ' * indent} #{v.inspect}") 32 | end 33 | io.write("#{' ' * indent}]") 34 | when Hash 35 | io.puts("{") 36 | self.pp(io, value, indent) 37 | io.write("#{' ' * indent}}") 38 | else 39 | io.write("#{value.inspect}") 40 | end 41 | io.puts 42 | end 43 | indent -= 2 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/requests/basic_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Excon::Connection do 4 | include_context('test server', :webrick, 'basic.ru', before: :start, after: :stop) 5 | context 'when an explicit uri is passed' do 6 | let(:conn) do 7 | Excon::Connection.new(host: '127.0.0.1', 8 | hostname: '127.0.0.1', 9 | nonblock: false, 10 | port: 9292, 11 | scheme: 'http', 12 | ssl_verify_peer: false) 13 | end 14 | 15 | describe '.new' do 16 | it 'returns an instance' do 17 | expect(conn).to be_instance_of Excon::Connection 18 | end 19 | end 20 | 21 | context "when :method is :get and :path is /content-length/100" do 22 | describe "#request" do 23 | let(:response) do 24 | response = conn.request(method: :get, path: '/content-length/100') 25 | end 26 | it 'returns an Excon::Response' do 27 | expect(response).to be_instance_of Excon::Response 28 | end 29 | describe Excon::Response do 30 | describe '#status' do 31 | it 'returns 200' do 32 | expect(response.status).to eq 200 33 | end 34 | end 35 | end 36 | end 37 | end 38 | include_examples 'a basic client' 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /tests/pipeline_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Pipelined Requests') do 2 | with_server('good') do 3 | 4 | tests('with default :persistent => true') do 5 | returns(%w{ 1 2 3 4 }, 'connection is persistent') do 6 | connection = Excon.new('http://127.0.0.1:9292', :persistent => true) 7 | 8 | ret = [] 9 | ret << connection.requests([ 10 | {:method => :get, :path => '/echo/request_count'}, 11 | {:method => :get, :path => '/echo/request_count'} 12 | ]).map(&:body) 13 | ret << connection.requests([ 14 | {:method => :get, :path => '/echo/request_count'}, 15 | {:method => :get, :path => '/echo/request_count'} 16 | ]).map(&:body) 17 | ret.flatten 18 | end 19 | end 20 | 21 | tests('with default :persistent => false') do 22 | returns(%w{ 1 2 1 2 }, 'connection is persistent per call to #requests') do 23 | connection = Excon.new('http://127.0.0.1:9292', :persistent => false) 24 | 25 | ret = [] 26 | ret << connection.requests([ 27 | {:method => :get, :path => '/echo/request_count'}, 28 | {:method => :get, :path => '/echo/request_count'} 29 | ]).map(&:body) 30 | ret << connection.requests([ 31 | {:method => :get, :path => '/echo/request_count'}, 32 | {:method => :get, :path => '/echo/request_count'} 33 | ]).map(&:body) 34 | ret.flatten 35 | end 36 | 37 | end 38 | 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /tests/middlewares/escape_path_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon Decompress Middleware') do 2 | env_init 3 | with_rackup('basic.ru') do 4 | tests('encoded uri passed to connection') do 5 | tests('GET /echo%20dirty').returns(200) do 6 | connection = Excon::Connection.new({ 7 | :host => '127.0.0.1', 8 | :hostname => '127.0.0.1', 9 | :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::EscapePath], 10 | :nonblock => false, 11 | :port => 9292, 12 | :scheme => 'http', 13 | :ssl_verify_peer => false 14 | }) 15 | response = connection.request(:method => :get, :path => '/echo%20dirty') 16 | response[:status] 17 | end 18 | end 19 | 20 | tests('unencoded uri passed to connection') do 21 | tests('GET /echo dirty').returns(200) do 22 | connection = Excon::Connection.new({ 23 | :host => '127.0.0.1', 24 | :hostname => '127.0.0.1', 25 | :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::EscapePath], 26 | :nonblock => false, 27 | :port => 9292, 28 | :scheme => 'http', 29 | :ssl_verify_peer => false 30 | }) 31 | response = connection.request(:method => :get, :path => '/echo dirty') 32 | response[:status] 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/requests/eof_requests_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Excon do 4 | context "when dispatching requests" do 5 | context('to a server that does not supply response headers') do 6 | include_context("test server", :exec, 'bad.rb', :before => :start, :after => :stop ) 7 | before(:all) do 8 | @conn = Excon.new('http://127.0.0.1:9292') 9 | end 10 | 11 | context('when no block is given') do 12 | it 'should rescue from an EOFError and return response' do 13 | body = @conn.request(:method => :get, :path => '/eof/no_content_length_and_no_chunking').body 14 | expect(body).to eq 'hello' 15 | end 16 | end 17 | 18 | context('when a block is given') do 19 | it 'should rescue from EOFError and return response' do 20 | body = "" 21 | response_block = lambda {|chunk, remaining, total| body << chunk } 22 | @conn.request(:method => :get, :path => '/eof/no_content_length_and_no_chunking', :response_block => response_block) 23 | expect(body).to eq 'hello' 24 | end 25 | end 26 | end 27 | 28 | context('to a server that prematurely aborts the request with no response') do 29 | include_context("test server", :exec, 'eof.rb', :before => :start, :after => :stop ) 30 | 31 | it 'should raise an EOFError' do 32 | expect { Excon.get('http://127.0.0.1:9292/eof') }.to raise_error(Excon::Errors::SocketError) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/excon/middlewares/decompress.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | module Middleware 4 | class Decompress < Excon::Middleware::Base 5 | def request_call(datum) 6 | unless datum.has_key?(:response_block) 7 | key = datum[:headers].keys.detect {|k| k.to_s.casecmp('Accept-Encoding') == 0 } || 'Accept-Encoding' 8 | if datum[:headers][key].to_s.empty? 9 | datum[:headers][key] = 'deflate, gzip' 10 | end 11 | end 12 | @stack.request_call(datum) 13 | end 14 | 15 | def response_call(datum) 16 | body = datum[:response][:body] 17 | unless datum.has_key?(:response_block) || body.nil? || body.empty? 18 | if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Encoding') == 0 } 19 | encodings = Utils.split_header_value(datum[:response][:headers][key]) 20 | if encoding = encodings.last 21 | if encoding.casecmp('deflate') == 0 22 | # assume inflate omits header 23 | datum[:response][:body] = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(body) 24 | encodings.pop 25 | elsif encoding.casecmp('gzip') == 0 || encoding.casecmp('x-gzip') == 0 26 | datum[:response][:body] = Zlib::GzipReader.new(StringIO.new(body)).read 27 | encodings.pop 28 | end 29 | datum[:response][:headers][key] = encodings.join(', ') 30 | end 31 | end 32 | end 33 | @stack.response_call(datum) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /tests/data/excon.cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAye/3HW9x78ixabx4Scc+ZxRG9dnGf1L9E3iro1Eh2QVAMAO0 3 | 6LgCsAP+OPQX7eKInAVvJHZIojbxu7dIPn9MpMuYrlkB0bc3DEUIfJTXIeLPYWvL 4 | TQKq31S1PcyXLZqf8RR3rkTfHv8ir4B/nCrdDfl34UDVnswCLelvtcpDsSG0Y2Ie 5 | Va2C8Yur3axtWhysCBVkyidMaY0+/l9s+bAp00YqYfw2XBuad1VImATBkxEoapFx 6 | ztb7aZg+ZkW0XjkcjnM9iKz/eBd7BTMEo8V2kf8251UzesYGOb6trZPbd4IMSdkb 7 | EyfLV2iWuhRByyi5mi69yYNyk+YkGU+x/pPgYQIDAQABAoIBAAFyvRzy7ahAkjUl 8 | 6t7slN/8Xz3oH+pN2A7JsMFtFYcO6JTvkd6RY0OL48jYx0sncr9bsp5aUs8HAdjM 9 | ybHZC92qsB+f98lfPP/ThuoNzzUpAT+7nCujN0J+wwX8b6EeGMOL2Afh6o+4WLFV 10 | hJTEIe21ukxdQKrw35sgr3JoTu/4QKYQRvg+MIeZ5KkBzi10JffesCSrGeHFn5Ka 11 | QjhXM7J31gfYPBIKNPDKKzP7jiDg15Y4C2SVxUWshMPErJwNWZWDvka81h/nxOeg 12 | woGPTszQ3f8gMOarebT2XW1Hyf1NzIqB/Pq5lTN+V7MVRq2qEeqDDeYA5uyP7G6B 13 | /q0pQ1kCgYEA45rFHrTMzWts9PoC0Usz4+1gKBVgPQ7guXOe7GWnTdLR5YkEkjFO 14 | +9CoGVNDMyGWyJsszDKMeeW3xdNSwvKlnQ7r9h20kMqNfMr/2f44wR5TqMf6J4x4 15 | KG7f1BY2nxnEBwPb+bceNf1iWqEyE5kOvhdW3HvsHjRtgKCRdDuJT0cCgYEA4yFv 16 | xZvjjmu+tU9BeZorjC31gJjdowgm4xqjuxPOA2YE4eO3akgPoEl5/RNSYYsYFBD0 17 | p3JC9UpBL243XfXyeH23UyOOS2oOdDTvjZrB5DodfuASC3ZE8kB7gYvO7VY7YbO5 18 | 3oUggBFJIBYsYw/sdTPhdfgvVcENVvs79lLetxcCgYBe0/f24CtIFs7xjkyoOKXo 19 | +9iJOsa7CbzVZ3e6VGwNP53V/W1bH5Ih1oCC9K7V2dPBxu02MoVWsEAN+wrLUF9x 20 | /WqQQzYc8VdbvBQ3FfU9eM4wTwlJevwBFJjK+2pMhWan6ZK4CvRaWDSnP+vmQGnl 21 | B0JYYIUy3HMXGU8g10FRNwKBgC41Pj0QovH1n53ZWvO3Vsa1Du4zq1ugq/CmGctU 22 | kyQD3FhscIMx1+P/Ls864BayKprifDOYvmCS08InhSIbiFHVGbfTUv6qu/gOUPIZ 23 | Gvsoi5mlUmZ8kDhMwBOx8s48LeaJnvXTVbJYUe0yNaONuMh7XpIWhOLIXMNmZ2OM 24 | pnzPAoGBALLr8/bBeTiST4u6YdMJW6AI8TelvksqVmG5tWo6xviMQZZG2OLnXlOV 25 | byEg/5hwNXrFxaTMbB1N8PDzkiHX29z2ioyLkyHnbWVgp1swAY4LdDy+NUGFl0bl 26 | nD3TMektFf8YT/YK1ttDXEotvpI90ZtM5lu85OFn17tCVOF5Q556 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tests/data/127.0.0.1.cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAn0O1JS7NIk552MLSXQ1ty2LIpPl6Kfp68g4rhCfKoXhQ3kdp 3 | OL6VmKRfmpe2JrXFSpaiQbiXoUT8WWapC56pYKRcVn62UNNNi3OBTeSKVR0Iqrrg 4 | DHOc31dqfKPDvqaDyrp+s598BfSB8hOIvdB/g3G87oAmEn3mpehndmj9fdH01v14 5 | xOlMUpFQG2puCGOs9wGzucYnoeOGImXca6Hudx2NyIZuMnDyh+mXoqodxoXcTPPS 6 | ZE1+T0gRM3wQ1WgyXcIlhYAJySDdEJ74hPVD1fJn2DvPtDNLopwyUXAy29eL+o3x 7 | rz/3wcuLTo4I3Cdu3/kfrm7xEvcY1pYKYyycVQIDAQABAoIBACUrSRhvbsKF7Bvx 8 | g+ThoHEqEcemzaMEaTMaqX9DRiAfE0h9BAqROBqSqhlLNOCd5Xh95UpPSVwC3J4E 9 | vokOs1rxoPcyxVvhpKRaBaBnKP2qM/6cdHOTe9YH8bs7ARD6jaF/mthS7T/6i2Uy 10 | t+QMx+WmYsCKudfw+CZaMeNJp3d4bVeqmpCbPlelV5fZBol4QhCn3mmLALoJB/sb 11 | 5CRG7mmEAjCKR8FtylOQVV9rp3tLvwsMIXCFybRkkUcg1KX6MNrwP4w+FFjxGOpt 12 | zCRTRBCiqCJFNdffgasOV4XkJ4kxM3tYhXfJzuMAl+qcP/17TmEcC5ppzaLQOTN/ 13 | EaJcXTUCgYEAzUQ7+MriUleFKXpkjIwWwFEnqN9w+a+nukSGTd7sgn+u+QxOiIBC 14 | 3eLz2mJcjz7nE27jiUoYT+v9i1R7ia/Y0ROshxRPNERVy/Jc7O7l1nTRWyPq2o4e 15 | 4eD91ZJ6yxhnPsmOhy6rkEBb8OAzySEhx9W3FNb8koo5UUlK0pH8Y/MCgYEAxqDM 16 | rTs5FuNegxm5MyxILcIWyBjKRT6CqaLb52OzWfukjDCeqRgrTmqRiG6IDlJGJ2d+ 17 | BnFW3rbfpI5Xnem1oMXF1wG1YplFwU4U8eReQRxCS03Gze5wPiEFrFQtxSLYyE2i 18 | sYOnn9jhntRhq5uzhiCP3BpSrzLUgj8kVattuJcCgYBqTwqYYcsxRi6rOr5UpEEs 19 | PVvC9GY4iqbdq5u7PNdVBvgB+EO8ImF/NYmB8yto7PUUdXvWoM9SpTBdNtX8i1xJ 20 | fF0AYJ5cvX+J7u39sATNOxcqksGRi8WmyrXsJL7/7JWcSRtOG/ey8AIuEJABfO1X 21 | 5/G4E1ggNQJWfRUJVZ5XzQKBgQDBAMSBW3eYvuw6VYd4qwGjvOGoIzaAXEJYlVA6 22 | oc1HlVaJhkDVLBAAjVrGiCiaSeWzKkPx/LWdiXL8kfefENYU17fquNVIboiyUa3W 23 | ccIIYG4mf/e6aIMWS74Yel9THA4rbAy3kqcRkouBavtQ5eVmYkOHauiHJswJl1M2 24 | D3J5FwKBgQC+u770fwc9o4jXtvICiu68wNkH+3pDy22uy5L+iHFq/RHZ94AjtwX+ 25 | 0cUF73CeSii1MBc00vVVJEQmBXAOAlnAGaekcqPzUOQvSL7UjqzGfprL5hf+CY/G 26 | BQ4bgdMDGtYZzmXg3wQ99rW0IDZFnJn4KfjaOPBpEfOBsVC5dB+UKw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /benchmarks/vs_stdlib.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' if RUBY_VERSION < '1.9' 2 | 3 | require 'sinatra/base' 4 | require 'tach' 5 | 6 | require File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib', 'excon') 7 | 8 | module Excon 9 | class Server < Sinatra::Base 10 | 11 | def self.run 12 | Rack::Handler::WEBrick.run( 13 | Excon::Server.new, 14 | :Port => 9292, 15 | :AccessLog => [], 16 | :Logger => WEBrick::Log.new(nil, WEBrick::Log::ERROR) 17 | ) 18 | end 19 | 20 | get '/data/:amount' do |amount| 21 | 'x' * amount.to_i 22 | end 23 | 24 | end 25 | end 26 | 27 | def with_server(&block) 28 | pid = Process.fork do 29 | Excon::Server.run 30 | end 31 | loop do 32 | sleep(1) 33 | begin 34 | Excon.get('http://localhost:9292/api/foo') 35 | break 36 | rescue 37 | end 38 | end 39 | yield 40 | ensure 41 | Process.kill(9, pid) 42 | end 43 | 44 | require 'net/http' 45 | require 'open-uri' 46 | 47 | url = 'http://localhost:9292/data/1000' 48 | 49 | with_server do 50 | 51 | Tach.meter(100) do 52 | 53 | tach('Excon') do 54 | Excon.get(url).body 55 | end 56 | 57 | # tach('Excon (persistent)') do |times| 58 | # excon = Excon.new(url) 59 | # times.times do 60 | # excon.request(:method => 'get').body 61 | # end 62 | # end 63 | 64 | tach('Net::HTTP') do 65 | # Net::HTTP.get('localhost', '/data/1000', 9292) 66 | Net::HTTP.start('localhost', 9292) {|http| http.get('/data/1000').body } 67 | end 68 | 69 | # tach('Net::HTTP (persistent)') do |times| 70 | # Net::HTTP.start('localhost', 9292) do |http| 71 | # times.times do 72 | # http.get('/data/1000').body 73 | # end 74 | # end 75 | # end 76 | 77 | # tach('open-uri') do 78 | # open(url).read 79 | # end 80 | 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/excon/middlewares/mock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | module Middleware 4 | class Mock < Excon::Middleware::Base 5 | def request_call(datum) 6 | if datum[:mock] 7 | # convert File/Tempfile body to string before matching: 8 | if datum[:body].respond_to?(:read) 9 | if datum[:body].respond_to?(:binmode) 10 | datum[:body].binmode 11 | end 12 | if datum[:body].respond_to?(:rewind) 13 | datum[:body].rewind 14 | end 15 | datum[:body] = datum[:body].read 16 | elsif !datum[:body].nil? && !datum[:body].is_a?(String) 17 | raise Excon::Errors::InvalidStub.new("Request body should be a string or an IO object. #{datum[:body].class} provided") 18 | end 19 | 20 | if stub = Excon.stub_for(datum) 21 | datum[:response] = { 22 | :body => '', 23 | :headers => {}, 24 | :status => 200, 25 | :remote_ip => '127.0.0.1' 26 | } 27 | 28 | stub_datum = case stub.last 29 | when Proc 30 | stub.last.call(datum) 31 | else 32 | stub.last 33 | end 34 | 35 | datum[:response].merge!(stub_datum.reject {|key,value| key == :headers}) 36 | if stub_datum.has_key?(:headers) 37 | datum[:response][:headers].merge!(stub_datum[:headers]) 38 | end 39 | elsif datum[:allow_unstubbed_requests] != true 40 | # if we reach here no stubs matched 41 | message = StringIO.new 42 | message.puts('no stubs matched') 43 | Excon::PrettyPrinter.pp(message, datum) 44 | raise(Excon::Errors::StubNotFound.new(message.string)) 45 | end 46 | end 47 | 48 | @stack.request_call(datum) 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /tests/middlewares/redirect_follower_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon redirector support') do 2 | env_init 3 | 4 | tests("request(:method => :get, :path => '/old').body").returns('new') do 5 | Excon.stub( 6 | { :path => '/old' }, 7 | { 8 | :headers => { 'Location' => 'http://127.0.0.1:9292/new' }, 9 | :body => 'old', 10 | :status => 301 11 | } 12 | ) 13 | 14 | Excon.stub( 15 | { :path => '/new' }, 16 | { 17 | :body => 'new', 18 | :status => 200 19 | } 20 | ) 21 | 22 | Excon.get( 23 | 'http://127.0.0.1:9292', 24 | :path => '/old', 25 | :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower], 26 | :mock => true 27 | ).body 28 | end 29 | 30 | env_restore 31 | end 32 | 33 | Shindo.tests('Excon redirect support for relative Location headers') do 34 | env_init 35 | 36 | tests("request(:method => :get, :path => '/old').body").returns('new') do 37 | Excon.stub( 38 | { :path => '/old' }, 39 | { 40 | :headers => { 'Location' => '/new' }, 41 | :body => 'old', 42 | :status => 301 43 | } 44 | ) 45 | 46 | Excon.stub( 47 | { :path => '/new' }, 48 | { 49 | :body => 'new', 50 | :status => 200 51 | } 52 | ) 53 | 54 | Excon.get( 55 | 'http://127.0.0.1:9292', 56 | :path => '/old', 57 | :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower], 58 | :mock => true 59 | ).body 60 | end 61 | 62 | env_restore 63 | end 64 | 65 | Shindo.tests("Excon redirecting post request") do 66 | env_init 67 | 68 | with_rackup('redirecting.ru') do 69 | tests("request not have content-length and body").returns('ok') do 70 | Excon.post( 71 | 'http://127.0.0.1:9292', 72 | :path => '/first', 73 | :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower], 74 | :body => "a=Some_content" 75 | ).body 76 | end 77 | end 78 | 79 | env_restore 80 | end 81 | -------------------------------------------------------------------------------- /lib/excon/headers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | class Headers < Hash 4 | 5 | SENTINEL = {} 6 | 7 | alias_method :raw_writer, :[]= 8 | alias_method :raw_reader, :[] 9 | if SENTINEL.respond_to?(:assoc) 10 | alias_method :raw_assoc, :assoc 11 | end 12 | alias_method :raw_delete, :delete 13 | alias_method :raw_fetch, :fetch 14 | alias_method :raw_has_key?, :has_key? 15 | alias_method :raw_include?, :include? 16 | alias_method :raw_key?, :key? 17 | alias_method :raw_member?, :member? 18 | alias_method :raw_merge, :merge 19 | alias_method :raw_merge!, :merge! 20 | alias_method :raw_rehash, :rehash 21 | alias_method :raw_store, :store 22 | alias_method :raw_values_at, :values_at 23 | 24 | def initialize 25 | @downcased = {} 26 | end 27 | 28 | def [](key) 29 | @downcased[key.to_s.downcase] 30 | end 31 | 32 | alias_method :[]=, :store 33 | def []=(key, value) 34 | raw_writer(key, value) 35 | @downcased[key.to_s.downcase] = value 36 | end 37 | 38 | if SENTINEL.respond_to? :assoc 39 | def assoc(obj) 40 | @downcased.assoc(obj.downcase) 41 | end 42 | end 43 | 44 | def delete(key, &proc) 45 | raw_delete(key, &proc) 46 | @downcased.delete(key.to_s.downcase, &proc) 47 | end 48 | 49 | def fetch(key, default = nil, &proc) 50 | if proc 51 | @downcased.fetch(key.to_s.downcase, &proc) 52 | else 53 | @downcased.fetch(key.to_s.downcase, default) 54 | end 55 | end 56 | 57 | alias_method :has_key?, :key? 58 | alias_method :has_key?, :member? 59 | def has_key?(key) 60 | raw_key?(key) || @downcased.has_key?(key.to_s.downcase) 61 | end 62 | 63 | def merge(other_hash) 64 | self.dup.merge!(other_hash) 65 | end 66 | 67 | def merge!(other_hash) 68 | other_hash.each do |key, value| 69 | self[key] = value 70 | end 71 | raw_merge!(other_hash) 72 | end 73 | 74 | def rehash 75 | @downcased.rehash 76 | raw_rehash 77 | end 78 | 79 | def values_at(*keys) 80 | @downcased.values_at(*keys.map {|key| key.to_s.downcase}) 81 | end 82 | 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/excon/middlewares/redirect_follower.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | module Middleware 4 | class RedirectFollower < Excon::Middleware::Base 5 | 6 | def get_header(datum, header) 7 | _, header_value = datum[:response][:headers].detect do |key, value| 8 | key.casecmp(header) == 0 9 | end 10 | header_value 11 | end 12 | 13 | def response_call(datum) 14 | if datum.has_key?(:response) 15 | case datum[:response][:status] 16 | when 301, 302, 303, 307, 308 17 | uri_parser = datum[:uri_parser] || Excon.defaults[:uri_parser] 18 | 19 | location = get_header(datum, 'Location') 20 | uri = uri_parser.parse(location) 21 | 22 | # delete old/redirect response 23 | response = datum.delete(:response) 24 | 25 | params = datum.dup 26 | params.delete(:connection) 27 | params.delete(:password) 28 | params.delete(:stack) 29 | params.delete(:user) 30 | 31 | if [301, 302, 303].include?(response[:status]) 32 | params[:method] = :get 33 | params.delete(:body) 34 | params[:headers].delete('Content-Length') 35 | end 36 | params[:headers] = datum[:headers].dup 37 | params[:headers].delete('Authorization') 38 | params[:headers].delete('Proxy-Connection') 39 | params[:headers].delete('Proxy-Authorization') 40 | params[:headers].delete('Host') 41 | params.merge!( 42 | :scheme => uri.scheme || datum[:scheme], 43 | :host => uri.host || datum[:host], 44 | :hostname => uri.hostname || datum[:hostname], 45 | :port => uri.port || datum[:port], 46 | :path => uri.path, 47 | :query => uri.query 48 | ) 49 | 50 | params.merge!(:user => Utils.unescape_uri(uri.user)) if uri.user 51 | params.merge!(:password => Utils.unescape_uri(uri.password)) if uri.password 52 | 53 | response = Excon::Connection.new(params).request 54 | datum.merge!({:response => response.data}) 55 | else 56 | @stack.response_call(datum) 57 | end 58 | else 59 | @stack.response_call(datum) 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /tests/request_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Request Tests') do 2 | with_server('good') do 3 | 4 | tests('persistent connections') do 5 | ip_ports = %w(127.0.0.1:9292) 6 | ip_ports << "[::1]:9293" unless RUBY_PLATFORM == 'java' 7 | ip_ports.each do |ip_port| 8 | 9 | tests("with default :persistent => true, #{ip_port}") do 10 | connection = nil 11 | 12 | returns(['1', '2'], 'uses a persistent connection') do 13 | connection = Excon.new("http://#{ip_port}", :persistent => true) 14 | 2.times.map do 15 | connection.request(:method => :get, :path => '/echo/request_count').body 16 | end 17 | end 18 | 19 | returns(['3', '1', '2'], ':persistent => false resets connection') do 20 | ret = [] 21 | ret << connection.request(:method => :get, 22 | :path => '/echo/request_count', 23 | :persistent => false).body 24 | ret << connection.request(:method => :get, 25 | :path => '/echo/request_count').body 26 | ret << connection.request(:method => :get, 27 | :path => '/echo/request_count').body 28 | end 29 | end 30 | 31 | tests("with default :persistent => false, #{ip_port}") do 32 | connection = nil 33 | 34 | returns(['1', '1'], 'does not use a persistent connection') do 35 | connection = Excon.new("http://#{ip_port}", :persistent => false) 36 | 2.times.map do 37 | connection.request(:method => :get, :path => '/echo/request_count').body 38 | end 39 | end 40 | 41 | returns(['1', '2', '3', '1'], ':persistent => true enables persistence') do 42 | ret = [] 43 | ret << connection.request(:method => :get, 44 | :path => '/echo/request_count', 45 | :persistent => true).body 46 | ret << connection.request(:method => :get, 47 | :path => '/echo/request_count', 48 | :persistent => true).body 49 | ret << connection.request(:method => :get, 50 | :path => '/echo/request_count').body 51 | ret << connection.request(:method => :get, 52 | :path => '/echo/request_count').body 53 | end 54 | end 55 | 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /tests/utils_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon::Utils') do 2 | 3 | tests('#connection_uri') do 4 | 5 | expected_uri = 'unix:///tmp/some.sock' 6 | tests('using UNIX scheme').returns(expected_uri) do 7 | connection = Excon.new('unix:///some/path', :socket => '/tmp/some.sock') 8 | Excon::Utils.connection_uri(connection.data) 9 | end 10 | 11 | tests('using HTTP scheme') do 12 | 13 | expected_uri = 'http://foo.com:80' 14 | tests('with default port').returns(expected_uri) do 15 | connection = Excon.new('http://foo.com/some/path') 16 | Excon::Utils.connection_uri(connection.data) 17 | end 18 | 19 | expected_uri = 'http://foo.com' 20 | tests('without default port').returns(expected_uri) do 21 | connection = Excon.new('http://foo.com/some/path', :omit_default_port => true) 22 | Excon::Utils.connection_uri(connection.data) 23 | end 24 | 25 | end 26 | 27 | end 28 | 29 | tests('#request_uri') do 30 | 31 | tests('using UNIX scheme') do 32 | 33 | expected_uri = 'unix:///tmp/some.sock/some/path' 34 | tests('without query').returns(expected_uri) do 35 | connection = Excon.new('unix:/', :socket => '/tmp/some.sock') 36 | params = { :path => '/some/path' } 37 | Excon::Utils.request_uri(connection.data.merge(params)) 38 | end 39 | 40 | expected_uri = 'unix:///tmp/some.sock/some/path?bar=that&foo=this' 41 | tests('with query').returns(expected_uri) do 42 | connection = Excon.new('unix:/', :socket => '/tmp/some.sock') 43 | params = { :path => '/some/path', :query => { :foo => 'this', :bar => 'that' } } 44 | Excon::Utils.request_uri(connection.data.merge(params)) 45 | end 46 | 47 | end 48 | 49 | tests('using HTTP scheme') do 50 | 51 | expected_uri = 'http://foo.com:80/some/path' 52 | tests('without query').returns(expected_uri) do 53 | connection = Excon.new('http://foo.com') 54 | params = { :path => '/some/path' } 55 | Excon::Utils.request_uri(connection.data.merge(params)) 56 | end 57 | 58 | expected_uri = 'http://foo.com:80/some/path?bar=that&foo=this' 59 | tests('with query').returns(expected_uri) do 60 | connection = Excon.new('http://foo.com') 61 | params = { :path => '/some/path', :query => { :foo => 'this', :bar => 'that' } } 62 | Excon::Utils.request_uri(connection.data.merge(params)) 63 | end 64 | 65 | end 66 | 67 | end 68 | 69 | tests('#escape_uri').returns('/hello%20excon') do 70 | Excon::Utils.escape_uri('/hello excon') 71 | end 72 | 73 | tests('#unescape_uri').returns('/hello excon') do 74 | Excon::Utils.unescape_uri('/hello%20excon') 75 | end 76 | 77 | tests('#unescape_form').returns('message=We love excon!') do 78 | Excon::Utils.unescape_form('message=We+love+excon!') 79 | end 80 | 81 | end 82 | -------------------------------------------------------------------------------- /spec/support/shared_contexts/test_server_context.rb: -------------------------------------------------------------------------------- 1 | # TODO: Clean up this doc and dry up the conditionals 2 | # 3 | # Required params: 4 | # plugin (e.g., webrick, unicorn, etc) 5 | # file (e.g. a rackup ) 6 | # 7 | # Optional params: 8 | # optional paramters may given as a hash 9 | # opts may contain a bind argument 10 | # opts may also contain before and after options 11 | # 12 | # In its simplest form: 13 | # { :before => :start, :after => :stop } 14 | # 15 | # With lambdas, which recieve @server as an argument 16 | # { before: lambda {|s| s.start }, after: lambda { |s| s.stop} } 17 | # 18 | # In both the cases above, before defaults to before(:all) 19 | # This can be circumvented with a Hash 20 | # { before: { :context => :start }, after: { :context => :stop } } 21 | # or 22 | # { before: { context: lambda { |s| s.start } }, after: { context: lambda { |s| s.stop } } } 23 | 24 | shared_context "test server" do |plugin, file, opts = {}| 25 | plugin = plugin.to_sym unless plugin.is_a? Symbol 26 | if plugin == :unicorn && RUBY_PLATFORM == "java" 27 | before { skip("until unicorn supports jruby") } 28 | end 29 | abs_file = Object.send("#{plugin}_path", file) 30 | args = { plugin => abs_file} 31 | args[:bind] = opts[:bind] if opts.key? :bind 32 | 33 | 34 | before_hook = opts.key?(:before) && (opts[:before].is_a?(Symbol) || opts[:before].is_a?(Proc) || opts[:before].is_a?(Hash)) 35 | 36 | if before_hook && opts[:before].is_a?(Hash) 37 | event = opts[:before].keys.first 38 | before(event) { 39 | @server = Excon::Test::Server.new(args) 40 | if opts[:before][event].is_a? Symbol 41 | @server.send(opts[:before][event]) 42 | else 43 | opts[:before][event].call(@server) 44 | end 45 | } 46 | elsif 47 | before(:all) { 48 | @server = Excon::Test::Server.new(args) 49 | before_hook = opts.key?(:before) && (opts[:before].is_a?(Symbol) || opts[:before].is_a?(Proc) || opts[:before].is_a?(Hash)) 50 | 51 | if before_hook 52 | if opts[:before].is_a? Symbol 53 | @server.send(opts[:before]) 54 | else 55 | opts[:before].call(@server) 56 | end 57 | end 58 | } 59 | end 60 | 61 | after_hook = opts.key?(:after) && (opts[:after].is_a?(Symbol) || opts[:after].is_a?(Proc) || opts[:after].is_a?(Hash)) 62 | 63 | if after_hook && opts[:after].is_a?(Hash) 64 | event = opts[:after].keys.first 65 | after(event) { 66 | if opts[:after][event].is_a? Symbol 67 | @server.send(opts[:after][event]) 68 | else 69 | opts[:after][event].call(@server) 70 | end 71 | } 72 | elsif after_hook 73 | after(:all) { 74 | if opts[:after].is_a? Symbol 75 | @server.send(opts[:after]) 76 | elsif opts[:after].is_a? Hash 77 | 78 | else 79 | opts[:after].call(@server) 80 | end 81 | } 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /benchmarks/headers_case_sensitivity.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stringio' 3 | require 'tach' 4 | 5 | def all_match_socket 6 | io = StringIO.new 7 | io << "Connection: close\n" 8 | io << "Content-Length: 000\n" 9 | io << "Content-Type: text/html\n" 10 | io << "Date: Xxx, 00 Xxx 0000 00:00:00 GMT\n" 11 | io << "Server: xxx\n" 12 | io << "Transfer-Encoding: chunked\n" 13 | io << "\n\n" 14 | io.rewind 15 | io 16 | end 17 | 18 | Formatador.display_line('all_match') 19 | Formatador.indent do 20 | Tach.meter(10_000) do 21 | tach('compare on read') do 22 | socket, headers = all_match_socket, {} 23 | until ((data = socket.readline).chop!).empty? 24 | key, value = data.split(': ') 25 | headers[key] = value 26 | (key.casecmp('Transfer-Encoding') == 0) && (value.casecmp('chunked') == 0) 27 | (key.casecmp('Connection') == 0) && (value.casecmp('close') == 0) 28 | (key.casecmp('Content-Length') == 0) 29 | end 30 | end 31 | 32 | tach('original') do 33 | socket, headers = all_match_socket, {} 34 | until ((data = socket.readline).chop!).empty? 35 | key, value = data.split(': ') 36 | headers[key] = value 37 | end 38 | headers.has_key?('Transfer-Encoding') && headers['Transfer-Encoding'].casecmp('chunked') == 0 39 | headers.has_key?('Connection') && headers['Connection'].casecmp('close') == 0 40 | headers.has_key?('Content-Length') 41 | end 42 | end 43 | end 44 | 45 | def none_match_socket 46 | io = StringIO.new 47 | io << "Cache-Control: max-age=0\n" 48 | io << "Content-Type: text/html\n" 49 | io << "Date: Xxx, 00 Xxx 0000 00:00:00 GMT\n" 50 | io << "Expires: Xxx, 00 Xxx 0000 00:00:00 GMT\n" 51 | io << "Last-Modified: Xxx, 00 Xxx 0000 00:00:00 GMT\n" 52 | io << "Server: xxx\n" 53 | io << "\n\n" 54 | io.rewind 55 | io 56 | end 57 | 58 | Formatador.display_line('none_match') 59 | Formatador.indent do 60 | Tach.meter(10_000) do 61 | tach('compare on read') do 62 | socket, headers = none_match_socket, {} 63 | until ((data = socket.readline).chop!).empty? 64 | key, value = data.split(': ') 65 | headers[key] = value 66 | (key.casecmp('Transfer-Encoding') == 0) && (value.casecmp('chunked') == 0) 67 | (key.casecmp('Connection') == 0) && (value.casecmp('close') == 0) 68 | (key.casecmp('Content-Length') == 0) 69 | end 70 | end 71 | 72 | tach('original') do 73 | socket, headers = none_match_socket, {} 74 | until ((data = socket.readline).chop!).empty? 75 | key, value = data.split(': ') 76 | headers[key] = value 77 | end 78 | headers.has_key?('Transfer-Encoding') && headers['Transfer-Encoding'].casecmp('chunked') == 0 79 | headers.has_key?('Connection') && headers['Connection'].casecmp('close') == 0 80 | headers.has_key?('Content-Length') 81 | end 82 | end 83 | end -------------------------------------------------------------------------------- /lib/excon/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | module Utils 4 | extend self 5 | 6 | CONTROL = (0x0..0x1f).map {|c| c.chr }.join + "\x7f" 7 | DELIMS = '<>#%"' 8 | UNWISE = '{}|\\^[]`' 9 | NONASCII = (0x80..0xff).map {|c| c.chr }.join 10 | UNESCAPED = /([#{ Regexp.escape(CONTROL + ' ' + DELIMS + UNWISE + NONASCII) }])/ 11 | ESCAPED = /%([0-9a-fA-F]{2})/ 12 | 13 | def connection_uri(datum = @data) 14 | unless datum 15 | raise ArgumentError, '`datum` must be given unless called on a Connection' 16 | end 17 | if datum[:scheme] == UNIX 18 | "#{datum[:scheme]}://#{datum[:socket]}" 19 | else 20 | "#{datum[:scheme]}://#{datum[:host]}#{port_string(datum)}" 21 | end 22 | end 23 | 24 | def request_uri(datum) 25 | connection_uri(datum) + datum[:path] + query_string(datum) 26 | end 27 | 28 | def port_string(datum) 29 | if datum[:port].nil? || (datum[:omit_default_port] && ((datum[:scheme].casecmp('http') == 0 && datum[:port] == 80) || (datum[:scheme].casecmp('https') == 0 && datum[:port] == 443))) 30 | '' 31 | else 32 | ':' + datum[:port].to_s 33 | end 34 | end 35 | 36 | def query_string(datum) 37 | str = String.new 38 | case datum[:query] 39 | when String 40 | str << '?' << datum[:query] 41 | when Hash 42 | str << '?' 43 | datum[:query].sort_by {|k,_| k.to_s }.each do |key, values| 44 | key = CGI.escape(key.to_s) 45 | if values.nil? 46 | str << key << '&' 47 | else 48 | [values].flatten.each do |value| 49 | str << key << '=' << CGI.escape(value.to_s) << '&' 50 | end 51 | end 52 | end 53 | str.chop! # remove trailing '&' 54 | end 55 | str 56 | end 57 | 58 | # Splits a header value +str+ according to HTTP specification. 59 | def split_header_value(str) 60 | return [] if str.nil? 61 | str = str.dup.strip 62 | str.force_encoding('BINARY') if FORCE_ENC 63 | str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]+)+) 64 | (?:,\s*|\Z)'xn).flatten 65 | end 66 | 67 | # Escapes HTTP reserved and unwise characters in +str+ 68 | def escape_uri(str) 69 | str = str.dup 70 | str.force_encoding('BINARY') if FORCE_ENC 71 | str.gsub(UNESCAPED) { "%%%02X" % $1[0].ord } 72 | end 73 | 74 | # Unescapes HTTP reserved and unwise characters in +str+ 75 | def unescape_uri(str) 76 | str = str.dup 77 | str.force_encoding('BINARY') if FORCE_ENC 78 | str.gsub(ESCAPED) { $1.hex.chr } 79 | end 80 | 81 | # Unescape form encoded values in +str+ 82 | def unescape_form(str) 83 | str = str.dup 84 | str.force_encoding('BINARY') if FORCE_ENC 85 | str.gsub!(/\+/, ' ') 86 | str.gsub(ESCAPED) { $1.hex.chr } 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /benchmarks/string_ranged_index.rb: -------------------------------------------------------------------------------- 1 | # Copied from my benchmark_hell repo: github.com/sgonyea/benchmark_hell 2 | 3 | require 'benchmark' 4 | 5 | iters = 1000000 6 | 7 | string = "Test String OMG" 8 | 9 | puts 'String ranged index vs. "coordinates"' 10 | Benchmark.bmbm do |x| 11 | x.report('ranged index') do 12 | iters.times.each do 13 | text = string[2..9] 14 | end 15 | end 16 | 17 | x.report('coordinates') do 18 | iters.times.each do 19 | text = string[2, 9] 20 | end 21 | end 22 | end 23 | 24 | =begin 25 | rvm exec bash -c 'echo && echo $RUBY_VERSION && echo && ruby string_ranged_index.rb' 26 | 27 | 28 | jruby-1.5.6 29 | 30 | String ranged index vs. "coordinates" 31 | Rehearsal ------------------------------------------------ 32 | ranged index 0.419000 0.000000 0.419000 ( 0.372000) 33 | coordinates 0.167000 0.000000 0.167000 ( 0.167000) 34 | --------------------------------------- total: 0.586000sec 35 | 36 | user system total real 37 | ranged index 0.158000 0.000000 0.158000 ( 0.159000) 38 | coordinates 0.125000 0.000000 0.125000 ( 0.125000) 39 | 40 | macruby-0.7.1 41 | 42 | String ranged index vs. "coordinates" 43 | Rehearsal ------------------------------------------------ 44 | ranged index 1.490000 0.030000 1.520000 ( 1.061326) 45 | coordinates 1.410000 0.030000 1.440000 ( 0.973640) 46 | --------------------------------------- total: 2.960000sec 47 | 48 | user system total real 49 | ranged index 1.520000 0.030000 1.550000 ( 1.081424) 50 | coordinates 1.480000 0.030000 1.510000 ( 1.029214) 51 | 52 | rbx-head 53 | 54 | String ranged index vs. "coordinates" 55 | Rehearsal ------------------------------------------------ 56 | ranged index 1.333304 0.009398 1.342702 ( 1.229629) 57 | coordinates 0.306087 0.000603 0.306690 ( 0.303538) 58 | --------------------------------------- total: 1.649392sec 59 | 60 | user system total real 61 | ranged index 0.923626 0.001597 0.925223 ( 0.927411) 62 | coordinates 0.298910 0.000533 0.299443 ( 0.300255) 63 | 64 | ruby-1.8.7-p330 65 | 66 | String ranged index vs. "coordinates" 67 | Rehearsal ------------------------------------------------ 68 | ranged index 0.730000 0.000000 0.730000 ( 0.738612) 69 | coordinates 0.660000 0.000000 0.660000 ( 0.660689) 70 | --------------------------------------- total: 1.390000sec 71 | 72 | user system total real 73 | ranged index 0.750000 0.000000 0.750000 ( 0.746172) 74 | coordinates 0.640000 0.000000 0.640000 ( 0.640687) 75 | 76 | ruby-1.9.2-p136 77 | 78 | String ranged index vs. "coordinates" 79 | Rehearsal ------------------------------------------------ 80 | ranged index 0.670000 0.000000 0.670000 ( 0.679046) 81 | coordinates 0.620000 0.000000 0.620000 ( 0.622257) 82 | --------------------------------------- total: 1.290000sec 83 | 84 | user system total real 85 | ranged index 0.680000 0.000000 0.680000 ( 0.686510) 86 | coordinates 0.620000 0.000000 0.620000 ( 0.624269) 87 | =end 88 | -------------------------------------------------------------------------------- /benchmarks/implicit_block-vs-explicit_block.rb: -------------------------------------------------------------------------------- 1 | # Copied from my benchmark_hell repo: github.com/sgonyea/benchmark_hell 2 | 3 | require 'benchmark' 4 | 5 | iters = 1000000 6 | 7 | def do_explicit(&block) 8 | var = "hello" 9 | block.call(var) 10 | end 11 | 12 | def do_implicit 13 | var = "hello" 14 | yield(var) 15 | end 16 | 17 | puts 'explicit block vs implicit' 18 | Benchmark.bmbm do |x| 19 | x.report('explicit') do 20 | iters.times.each do 21 | do_explicit {|var| 22 | var << "goodbye" 23 | } 24 | end 25 | end 26 | 27 | x.report('implicit') do 28 | iters.times.each do 29 | do_implicit {|var| 30 | var << "goodbye" 31 | } 32 | end 33 | end 34 | end 35 | 36 | =begin 37 | rvm exec bash -c 'echo && echo $RUBY_VERSION && echo && ruby implicit_block-vs-explicit_block.rb' 38 | 39 | jruby-1.5.6 40 | 41 | explicit block vs implicit 42 | Rehearsal -------------------------------------------- 43 | explicit 1.163000 0.000000 1.163000 ( 1.106000) 44 | implicit 0.499000 0.000000 0.499000 ( 0.499000) 45 | ----------------------------------- total: 1.662000sec 46 | 47 | user system total real 48 | explicit 0.730000 0.000000 0.730000 ( 0.730000) 49 | implicit 0.453000 0.000000 0.453000 ( 0.453000) 50 | 51 | macruby-0.7.1 52 | 53 | explicit block vs implicit 54 | Rehearsal -------------------------------------------- 55 | explicit 5.070000 0.130000 5.200000 ( 3.546388) 56 | implicit 3.140000 0.050000 3.190000 ( 2.255986) 57 | ----------------------------------- total: 8.390000sec 58 | 59 | user system total real 60 | explicit 5.340000 0.140000 5.480000 ( 3.774963) 61 | implicit 3.170000 0.060000 3.230000 ( 2.279951) 62 | 63 | rbx-head 64 | 65 | explicit block vs implicit 66 | Rehearsal -------------------------------------------- 67 | explicit 1.270136 0.006507 1.276643 ( 1.181588) 68 | implicit 0.839831 0.002203 0.842034 ( 0.820849) 69 | ----------------------------------- total: 2.118677sec 70 | 71 | user system total real 72 | explicit 0.960593 0.001526 0.962119 ( 0.966404) 73 | implicit 0.700361 0.001126 0.701487 ( 0.703591) 74 | 75 | ruby-1.8.7-p330 76 | 77 | explicit block vs implicit 78 | Rehearsal -------------------------------------------- 79 | explicit 3.970000 0.000000 3.970000 ( 3.985157) 80 | implicit 1.560000 0.000000 1.560000 ( 1.567599) 81 | ----------------------------------- total: 5.530000sec 82 | 83 | user system total real 84 | explicit 3.990000 0.010000 4.000000 ( 4.002637) 85 | implicit 1.560000 0.000000 1.560000 ( 1.560901) 86 | 87 | ruby-1.9.2-p136 88 | 89 | explicit block vs implicit 90 | Rehearsal -------------------------------------------- 91 | explicit 2.620000 0.010000 2.630000 ( 2.633762) 92 | implicit 1.080000 0.000000 1.080000 ( 1.076809) 93 | ----------------------------------- total: 3.710000sec 94 | 95 | user system total real 96 | explicit 2.630000 0.010000 2.640000 ( 2.637658) 97 | implicit 1.070000 0.000000 1.070000 ( 1.073589) 98 | =end 99 | -------------------------------------------------------------------------------- /lib/excon/test/server.rb: -------------------------------------------------------------------------------- 1 | require 'open4' 2 | require 'excon' 3 | require 'excon/test/plugin/server/webrick' 4 | require 'excon/test/plugin/server/unicorn' 5 | require 'excon/test/plugin/server/puma' 6 | require 'excon/test/plugin/server/exec' 7 | 8 | 9 | module Excon 10 | module Test 11 | class Server 12 | attr_accessor :app, :server, :bind, :pid, :read, :write, :error, :started_at, :timeout 13 | 14 | # Methods that must be implemented by a plugin 15 | INSTANCE_REQUIRES = [:start] 16 | Excon.defaults.merge!( 17 | connect_timeout: 5, 18 | read_timeout: 5, 19 | write_timeout: 5 20 | ) 21 | 22 | def initialize(args) 23 | # TODO: Validate these args 24 | @server = args.keys.first 25 | @app = args[server] 26 | args[:bind] ||= 'tcp://127.0.0.1:9292' 27 | @bind = URI.parse(args[:bind]) 28 | @is_unix_socket = (@bind.scheme == 'unix') 29 | @bind.host = @bind.host.gsub(/[\[\]]/, '') unless @is_unix_socket 30 | if args[:timeout] 31 | @timeout = args[:timeout] 32 | else 33 | @timeout = 20 34 | end 35 | name = @server.to_s.split('_').collect(&:capitalize).join 36 | plug = nested_const_get("Excon::Test::Plugin::Server::#{name}") 37 | self.extend plug 38 | check_implementation(plug) 39 | end 40 | 41 | def open_process(*args) 42 | if RUBY_PLATFORM == 'java' 43 | @pid, @write, @read, @error = IO.popen4(*args) 44 | else 45 | GC.disable if RUBY_VERSION < '1.9' 46 | @pid, @write, @read, @error = Open4.popen4(*args) 47 | end 48 | @started_at = Time.now 49 | end 50 | 51 | def elapsed_time 52 | Time.now - started_at 53 | end 54 | 55 | def stop 56 | if RUBY_PLATFORM == 'java' 57 | Process.kill('USR1', pid) 58 | else 59 | Process.kill(9, pid) 60 | GC.enable if RUBY_VERSION < '1.9' 61 | Process.wait(pid) 62 | end 63 | 64 | if @is_unix_socket 65 | socket = @bind.path 66 | File.delete(socket) if File.exist?(socket) 67 | end 68 | 69 | # TODO: Ensure process is really dead 70 | dump_errors 71 | true 72 | end 73 | def dump_errors 74 | lines = error.read.split($/) 75 | while line = lines.shift 76 | case line 77 | when /(ERROR|Error)/ 78 | unless line =~ /(null cert chain|did not return a certificate|SSL_read:: internal error)/ 79 | in_err = true 80 | puts 81 | end 82 | when /^(127|localhost)/ 83 | in_err = false 84 | end 85 | puts line if in_err 86 | end 87 | end 88 | 89 | private 90 | 91 | def nested_const_get(namespace) 92 | namespace.split('::').inject(Object) do |mod, klass| 93 | mod.const_get(klass) 94 | end 95 | end 96 | 97 | def check_implementation(plug) 98 | INSTANCE_REQUIRES.each do |m| 99 | unless self.respond_to? m 100 | raise "FATAL: #{plug} does not implement ##{m}" 101 | end 102 | end 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /tests/query_string_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon query string variants') do 2 | with_rackup('query_string.ru') do 3 | connection = Excon.new('http://127.0.0.1:9292') 4 | 5 | tests(":query => {:foo => 'bar'}") do 6 | response = connection.request(:method => :get, :path => '/query', :query => {:foo => 'bar'}) 7 | query_string = response.body[7..-1] # query string sent 8 | 9 | tests("query string sent").returns('foo=bar') do 10 | query_string 11 | end 12 | end 13 | 14 | tests(":query => {:foo => nil}") do 15 | response = connection.request(:method => :get, :path => '/query', :query => {:foo => nil}) 16 | query_string = response.body[7..-1] # query string sent 17 | 18 | tests("query string sent").returns('foo') do 19 | query_string 20 | end 21 | end 22 | 23 | tests(":query => {:foo => 'bar', :me => nil}") do 24 | response = connection.request(:method => :get, :path => '/query', :query => {:foo => 'bar', :me => nil}) 25 | query_string = response.body[7..-1] # query string sent 26 | 27 | test("query string sent includes 'foo=bar'") do 28 | query_string.split('&').include?('foo=bar') 29 | end 30 | 31 | test("query string sent includes 'me'") do 32 | query_string.split('&').include?('me') 33 | end 34 | end 35 | 36 | tests(":query => {:foo => 'bar', :me => 'too'}") do 37 | response = connection.request(:method => :get, :path => '/query', :query => {:foo => 'bar', :me => 'too'}) 38 | query_string = response.body[7..-1] # query string sent 39 | 40 | test("query string sent includes 'foo=bar'") do 41 | query_string.split('&').include?('foo=bar') 42 | end 43 | 44 | test("query string sent includes 'me=too'") do 45 | query_string.split('&').include?('me=too') 46 | end 47 | end 48 | 49 | # You can use an atom or a string for the hash keys, what is shown here is emulating 50 | # the Rails and PHP style of serializing a query array with a square brackets suffix. 51 | tests(":query => {'foo[]' => ['bar', 'baz'], :me => 'too'}") do 52 | response = connection.request(:method => :get, :path => '/query', :query => {'foo[]' => ['bar', 'baz'], :me => 'too'}) 53 | query_string = response.body[7..-1] # query string sent 54 | 55 | test("query string sent includes 'foo%5B%5D=bar'") do 56 | query_string.split('&').include?('foo%5B%5D=bar') 57 | end 58 | 59 | test("query string sent includes 'foo%5B%5D=baz'") do 60 | query_string.split('&').include?('foo%5B%5D=baz') 61 | end 62 | 63 | test("query string sent includes 'me=too'") do 64 | query_string.split('&').include?('me=too') 65 | end 66 | end 67 | 68 | tests(":query => {'foo%=#' => 'bar%=#'}") do 69 | response = connection.request(:method => :get, :path => '/query', :query => {'foo%=#' => 'bar%=#'}) 70 | query_string = response.body[7..-1] # query string sent 71 | 72 | tests("query string sent").returns('foo%25%3D%23=bar%25%3D%23') do 73 | query_string 74 | end 75 | end 76 | 77 | tests(":query => {'foo%=#' => nil}") do 78 | response = connection.request(:method => :get, :path => '/query', :query => {'foo%=#' => nil}) 79 | query_string = response.body[7..-1] # query string sent 80 | 81 | tests("query string sent").returns('foo%25%3D%23') do 82 | query_string 83 | end 84 | end 85 | 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /benchmarks/strip_newline.rb: -------------------------------------------------------------------------------- 1 | # require 'benchmark' 2 | # 3 | # COUNT = 1_000_000 4 | # data = "Content-Length: 100\r\n" 5 | # Benchmark.bmbm(25) do |bench| 6 | # bench.report('chomp') do 7 | # COUNT.times do 8 | # data = "Content-Length: 100\r\n" 9 | # data.chomp 10 | # end 11 | # end 12 | # bench.report('chomp!') do 13 | # COUNT.times do 14 | # data = "Content-Length: 100\r\n" 15 | # data.chomp! 16 | # end 17 | # end 18 | # bench.report('chop') do 19 | # COUNT.times do 20 | # data = "Content-Length: 100\r\n" 21 | # data.chop 22 | # end 23 | # end 24 | # bench.report('chop!') do 25 | # COUNT.times do 26 | # data = "Content-Length: 100\r\n" 27 | # data.chop! 28 | # end 29 | # end 30 | # bench.report('strip') do 31 | # COUNT.times do 32 | # data = "Content-Length: 100\r\n" 33 | # data.strip 34 | # end 35 | # end 36 | # bench.report('strip!') do 37 | # COUNT.times do 38 | # data = "Content-Length: 100\r\n" 39 | # data.strip! 40 | # end 41 | # end 42 | # bench.report('index') do 43 | # COUNT.times do 44 | # data = "Content-Length: 100\r\n" 45 | # data[0..-3] 46 | # end 47 | # end 48 | # end 49 | 50 | 51 | 52 | # Rehearsal ------------------------------------------------------------ 53 | # chomp 0.640000 0.000000 0.640000 ( 0.644043) 54 | # chomp! 0.530000 0.000000 0.530000 ( 0.531415) 55 | # chop 0.620000 0.000000 0.620000 ( 0.624321) 56 | # chop! 0.500000 0.000000 0.500000 ( 0.509146) 57 | # strip 0.640000 0.000000 0.640000 ( 0.638785) 58 | # strip! 0.530000 0.000000 0.530000 ( 0.532196) 59 | # index 0.740000 0.000000 0.740000 ( 0.745742) 60 | # --------------------------------------------------- total: 4.200000sec 61 | # 62 | # user system total real 63 | # chomp 0.640000 0.010000 0.650000 ( 0.647287) 64 | # chomp! 0.530000 0.000000 0.530000 ( 0.532868) 65 | # chop 0.630000 0.000000 0.630000 ( 0.628236) 66 | # chop! 0.520000 0.000000 0.520000 ( 0.522950) 67 | # strip 0.640000 0.000000 0.640000 ( 0.646328) 68 | # strip! 0.520000 0.000000 0.520000 ( 0.532715) 69 | # index 0.740000 0.010000 0.750000 ( 0.771277) 70 | 71 | require 'rubygems' 72 | require 'tach' 73 | 74 | data = "Content-Length: 100\r\n" 75 | Tach.meter(1_000_000) do 76 | tach('chomp') do 77 | data.dup.chomp 78 | end 79 | tach('chomp!') do 80 | data.dup.chomp! 81 | end 82 | tach('chop') do 83 | data.dup.chop 84 | end 85 | tach('chop!') do 86 | data.dup.chop! 87 | end 88 | tach('strip') do 89 | data.dup.strip 90 | end 91 | tach('strip!') do 92 | data.dup.strip! 93 | end 94 | tach('index') do 95 | data.dup[0..-3] 96 | end 97 | end 98 | 99 | # +--------+----------+----------+ 100 | # | tach | average | total | 101 | # +--------+----------+----------+ 102 | # | chomp | 1.444547 | 1.444547 | 103 | # +--------+----------+----------+ 104 | # | chomp! | 1.276813 | 1.276813 | 105 | # +--------+----------+----------+ 106 | # | chop | 1.422744 | 1.422744 | 107 | # +--------+----------+----------+ 108 | # | chop! | 1.240941 | 1.240941 | 109 | # +--------+----------+----------+ 110 | # | strip | 1.444776 | 1.444776 | 111 | # +--------+----------+----------+ 112 | # | strip! | 1.266459 | 1.266459 | 113 | # +--------+----------+----------+ 114 | # | index | 1.557975 | 1.557975 | 115 | # +--------+----------+----------+ -------------------------------------------------------------------------------- /tests/header_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon response header support') do 2 | env_init 3 | 4 | tests('Excon::Headers storage') do 5 | headers = Excon::Headers.new 6 | headers['Exact-Case'] = 'expected' 7 | headers['Another-Fixture'] = 'another' 8 | 9 | tests('stores and retrieves as received').returns('expected') do 10 | headers['Exact-Case'] 11 | end 12 | 13 | tests('enumerates keys as received') do 14 | ks = headers.keys 15 | tests('contains Exact-Case').returns(true) { ks.include? 'Exact-Case' } 16 | tests('contains Another-Fixture').returns(true) { ks.include? 'Another-Fixture' } 17 | end 18 | 19 | tests('supports case-insensitive access').returns('expected') do 20 | headers['EXACT-CASE'] 21 | end 22 | 23 | tests('but still returns nil for missing keys').returns(nil) do 24 | headers['Missing-Header'] 25 | end 26 | 27 | tests('Hash methods that should support case-insensitive access') do 28 | if {}.respond_to? :assoc 29 | tests('#assoc').returns(%w{exact-case expected}) do 30 | headers.assoc('exact-Case') 31 | end 32 | end 33 | 34 | tests('#delete') do 35 | tests('with just a key').returns('yes') do 36 | headers['Extra'] = 'yes' 37 | headers.delete('extra') 38 | end 39 | 40 | tests('with a proc').returns('called with notpresent') do 41 | headers.delete('notpresent') { |k| "called with #{k}" } 42 | end 43 | end 44 | 45 | tests('#fetch') do 46 | tests('when present').returns('expected') { headers.fetch('exact-CASE') } 47 | tests('with a default value').returns('default') { headers.fetch('missing', 'default') } 48 | tests('with a default proc').returns('got missing') do 49 | headers.fetch('missing') { |k| "got #{k}" } 50 | end 51 | end 52 | 53 | tests('#has_key?') do 54 | tests('when present').returns(true) { headers.has_key?('EXACT-case') } 55 | tests('when absent').returns(false) { headers.has_key?('missing') } 56 | end 57 | 58 | tests('#values_at') do 59 | tests('all present').returns(%w{expected another}) do 60 | headers.values_at('exACT-cASE', 'anotheR-fixturE') 61 | end 62 | tests('some missing').returns(['expected', nil]) do 63 | headers.values_at('exact-case', 'missing-header') 64 | end 65 | end 66 | end 67 | end 68 | 69 | with_rackup('response_header.ru') do 70 | 71 | tests('Response#get_header') do 72 | connection = nil 73 | response = nil 74 | 75 | tests('with variable header capitalization') do 76 | 77 | tests('response.get_header("mixedcase-header")').returns('MixedCase') do 78 | connection = Excon.new('http://foo.com:8080', :proxy => 'http://127.0.0.1:9292') 79 | response = connection.request(:method => :get, :path => '/foo') 80 | 81 | response.get_header("mixedcase-header") 82 | end 83 | 84 | tests('response.get_header("uppercase-header")').returns('UPPERCASE') do 85 | response.get_header("uppercase-header") 86 | end 87 | 88 | tests('response.get_header("lowercase-header")').returns('lowercase') do 89 | response.get_header("lowercase-header") 90 | end 91 | 92 | end 93 | 94 | tests('when provided key capitalization varies') do 95 | 96 | tests('response.get_header("MIXEDCASE-HEADER")').returns('MixedCase') do 97 | response.get_header("MIXEDCASE-HEADER") 98 | end 99 | 100 | tests('response.get_header("MiXeDcAsE-hEaDeR")').returns('MixedCase') do 101 | response.get_header("MiXeDcAsE-hEaDeR") 102 | end 103 | 104 | end 105 | 106 | tests('when header is unavailable') do 107 | 108 | tests('response.get_header("missing")').returns(nil) do 109 | response.get_header("missing") 110 | end 111 | 112 | end 113 | 114 | end 115 | 116 | end 117 | 118 | env_restore 119 | end 120 | -------------------------------------------------------------------------------- /benchmarks/excon_vs.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' if RUBY_VERSION < '1.9' 2 | require 'bundler' 3 | 4 | Bundler.require(:default) 5 | Bundler.require(:benchmark) 6 | 7 | require 'sinatra/base' 8 | 9 | require File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib', 'excon') 10 | 11 | module Excon 12 | class Server < Sinatra::Base 13 | 14 | def self.run 15 | Rack::Handler::WEBrick.run( 16 | Excon::Server.new, 17 | :Port => 9292, 18 | :AccessLog => [], 19 | :Logger => WEBrick::Log.new(nil, WEBrick::Log::ERROR) 20 | ) 21 | end 22 | 23 | get '/data/:amount' do |amount| 24 | 'x' * amount.to_i 25 | end 26 | 27 | end 28 | end 29 | 30 | def with_server(&block) 31 | pid = Process.fork do 32 | Excon::Server.run 33 | end 34 | loop do 35 | sleep(1) 36 | begin 37 | Excon.get('http://localhost:9292/api/foo') 38 | break 39 | rescue 40 | end 41 | end 42 | yield 43 | ensure 44 | Process.kill(9, pid) 45 | end 46 | 47 | require 'em-http-request' 48 | require 'httparty' 49 | require 'net/http' 50 | require 'open-uri' 51 | require 'rest_client' 52 | require 'tach' 53 | require 'typhoeus' 54 | 55 | size = 10_000 56 | path = '/data/' << size.to_s 57 | url = 'http://localhost:9292' << path 58 | 59 | times = 1_000 60 | 61 | with_server do 62 | 63 | Tach.meter(times) do 64 | 65 | tach('curb (persistent)') do |n| 66 | curb = Curl::Easy.new 67 | 68 | n.times do 69 | curb.url = url 70 | curb.http_get 71 | curb.body_str 72 | end 73 | end 74 | 75 | tach('em-http-request') do |n| 76 | EventMachine.run { 77 | count = 0 78 | 79 | n.times do 80 | http = EventMachine::HttpRequest.new(url).get 81 | 82 | http.callback { 83 | http.response 84 | count += 1 85 | EM.stop if count == n 86 | } 87 | 88 | http.errback { 89 | http.response 90 | count += 1 91 | EM.stop if count == n 92 | } 93 | end 94 | } 95 | end 96 | 97 | tach('Excon') do 98 | Excon.get(url).body 99 | end 100 | 101 | excon = Excon.new(url) 102 | tach('Excon (persistent)') do 103 | excon.request(:method => 'get').body 104 | end 105 | 106 | tach('HTTParty') do 107 | HTTParty.get(url).body 108 | end 109 | 110 | tach('Net::HTTP') do 111 | # Net::HTTP.get('localhost', path, 9292) 112 | Net::HTTP.start('localhost', 9292) {|http| http.get(path).body } 113 | end 114 | 115 | Net::HTTP.start('localhost', 9292) do |http| 116 | tach('Net::HTTP (persistent)') do 117 | http.get(path).body 118 | end 119 | end 120 | 121 | tach('open-uri') do 122 | open(url).read 123 | end 124 | 125 | tach('RestClient') do 126 | RestClient.get(url) 127 | end 128 | 129 | streamly = StreamlyFFI::Connection.new 130 | tach('StreamlyFFI (persistent)') do 131 | streamly.get(url) 132 | end 133 | 134 | tach('Typhoeus') do 135 | Typhoeus::Request.get(url).body 136 | end 137 | 138 | end 139 | end 140 | 141 | # +--------------------------+----------+ 142 | # | tach | total | 143 | # +--------------------------+----------+ 144 | # | Excon (persistent) | 1.529095 | 145 | # +--------------------------+----------+ 146 | # | curb (persistent) | 1.740387 | 147 | # +--------------------------+----------+ 148 | # | Typhoeus | 1.876236 | 149 | # +--------------------------+----------+ 150 | # | Excon | 2.001858 | 151 | # +--------------------------+----------+ 152 | # | StreamlyFFI (persistent) | 2.200701 | 153 | # +--------------------------+----------+ 154 | # | Net::HTTP | 2.395704 | 155 | # +--------------------------+----------+ 156 | # | Net::HTTP (persistent) | 2.418099 | 157 | # +--------------------------+----------+ 158 | # | HTTParty | 2.659317 | 159 | # +--------------------------+----------+ 160 | # | RestClient | 2.958159 | 161 | # +--------------------------+----------+ 162 | # | open-uri | 2.987051 | 163 | # +--------------------------+----------+ 164 | # | em-http-request | 4.123798 | 165 | # +--------------------------+----------+ 166 | -------------------------------------------------------------------------------- /lib/excon/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | 4 | VERSION = '0.57.0' 5 | 6 | CR_NL = "\r\n" 7 | 8 | DEFAULT_CA_FILE = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "data", "cacert.pem")) 9 | 10 | DEFAULT_CHUNK_SIZE = 1048576 # 1 megabyte 11 | 12 | # avoid overwrite if somebody has redefined 13 | unless const_defined?(:CHUNK_SIZE) 14 | CHUNK_SIZE = DEFAULT_CHUNK_SIZE 15 | end 16 | 17 | DEFAULT_RETRY_LIMIT = 4 18 | 19 | FORCE_ENC = CR_NL.respond_to?(:force_encoding) 20 | 21 | HTTP_1_1 = " HTTP/1.1\r\n" 22 | 23 | HTTP_VERBS = %w{connect delete get head options patch post put trace} 24 | 25 | HTTPS = 'https' 26 | 27 | NO_ENTITY = [204, 205, 304].freeze 28 | 29 | REDACTED = 'REDACTED' 30 | 31 | UNIX = 'unix' 32 | 33 | USER_AGENT = "excon/#{VERSION}" 34 | 35 | VERSIONS = "#{USER_AGENT} (#{RUBY_PLATFORM}) ruby/#{RUBY_VERSION}" 36 | 37 | VALID_REQUEST_KEYS = [ 38 | :body, 39 | :captures, 40 | :chunk_size, 41 | :debug_request, 42 | :debug_response, 43 | :expects, 44 | :headers, 45 | :idempotent, 46 | :instrumentor, 47 | :instrumentor_name, 48 | :method, 49 | :middlewares, 50 | :mock, 51 | :path, 52 | :persistent, 53 | :pipeline, 54 | :query, 55 | :read_timeout, 56 | :request_block, 57 | :response_block, 58 | :retries_remaining, # used internally 59 | :retry_limit, 60 | :versions, 61 | :write_timeout 62 | ] 63 | 64 | VALID_CONNECTION_KEYS = VALID_REQUEST_KEYS + [ 65 | :ciphers, 66 | :client_key, 67 | :client_key_data, 68 | :client_key_pass, 69 | :client_cert, 70 | :client_cert_data, 71 | :certificate, 72 | :certificate_path, 73 | :disable_proxy, 74 | :private_key, 75 | :private_key_path, 76 | :connect_timeout, 77 | :family, 78 | :host, 79 | :hostname, 80 | :omit_default_port, 81 | :nonblock, 82 | :reuseaddr, 83 | :password, 84 | :port, 85 | :proxy, 86 | :scheme, 87 | :socket, 88 | :ssl_ca_file, 89 | :ssl_ca_path, 90 | :ssl_cert_store, 91 | :ssl_verify_callback, 92 | :ssl_verify_peer, 93 | :ssl_verify_peer_host, 94 | :ssl_version, 95 | :tcp_nodelay, 96 | :thread_safe_sockets, 97 | :uri_parser, 98 | :user 99 | ] 100 | 101 | unless ::IO.const_defined?(:WaitReadable) 102 | class ::IO 103 | module WaitReadable; end 104 | end 105 | end 106 | 107 | unless ::IO.const_defined?(:WaitWritable) 108 | class ::IO 109 | module WaitWritable; end 110 | end 111 | end 112 | # these come last as they rely on the above 113 | DEFAULTS = { 114 | :chunk_size => CHUNK_SIZE || DEFAULT_CHUNK_SIZE, 115 | # see https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29 116 | :ciphers => 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS', 117 | :connect_timeout => 60, 118 | :debug_request => false, 119 | :debug_response => false, 120 | :headers => { 121 | 'User-Agent' => USER_AGENT 122 | }, 123 | :idempotent => false, 124 | :instrumentor_name => 'excon', 125 | :middlewares => [ 126 | Excon::Middleware::ResponseParser, 127 | Excon::Middleware::Expects, 128 | Excon::Middleware::Idempotent, 129 | Excon::Middleware::Instrumentor, 130 | Excon::Middleware::Mock 131 | ], 132 | :mock => false, 133 | :nonblock => true, 134 | :omit_default_port => false, 135 | :persistent => false, 136 | :read_timeout => 60, 137 | :retry_limit => DEFAULT_RETRY_LIMIT, 138 | :ssl_verify_peer => true, 139 | :ssl_uri_schemes => [HTTPS], 140 | :stubs => :global, 141 | :tcp_nodelay => false, 142 | :thread_safe_sockets => true, 143 | :uri_parser => URI, 144 | :versions => VERSIONS, 145 | :write_timeout => 60 146 | } 147 | 148 | end 149 | -------------------------------------------------------------------------------- /tests/error_tests.rb: -------------------------------------------------------------------------------- 1 | 2 | 3 | Shindo.tests('HTTPStatusError request/response debugging') do 4 | 5 | # Regression against e300458f2d9330cb265baeb8973120d08c665d9 6 | tests('Excon::Error knows about pertinent errors') do 7 | expected = [ 8 | 100, 9 | 101, 10 | (200..206).to_a, 11 | (300..307).to_a, 12 | (400..417).to_a, 13 | 422, 14 | 429, 15 | (500..504).to_a 16 | ] 17 | expected.flatten == Excon::Error.status_errors.keys 18 | end 19 | 20 | tests('new returns an Error').returns(true) do 21 | Excon::Error.new('bar').class == Excon::Error 22 | end 23 | 24 | tests('new raises errors for bad URIs').returns(true) do 25 | begin 26 | Excon.new('foo') 27 | false 28 | rescue => err 29 | err.to_s.include? 'foo' 30 | end 31 | end 32 | 33 | tests('new raises errors for bad paths').returns(true) do 34 | begin 35 | Excon.new('http://localhost', path: "foo\r\nbar: baz") 36 | false 37 | rescue => err 38 | err.to_s.include? "foo\r\nbar: baz" 39 | end 40 | end 41 | 42 | tests('can raise standard error and catch standard error').returns(true) do 43 | begin 44 | raise Excon::Error::Client.new('foo') 45 | rescue Excon::Error => e 46 | true 47 | end 48 | end 49 | 50 | tests('can raise legacy errors and catch legacy errors').returns(true) do 51 | begin 52 | raise Excon::Errors::Error.new('bar') 53 | rescue Excon::Errors::Error => e 54 | true 55 | end 56 | end 57 | 58 | tests('can raise standard error and catch legacy errors').returns(true) do 59 | begin 60 | raise Excon::Error::NotFound.new('bar') 61 | rescue Excon::Errors::Error => e 62 | true 63 | end 64 | end 65 | 66 | tests('can raise with status_error() and catch with standard error').returns(true) do 67 | begin 68 | raise Excon::Error.status_error({expects: 200}, {status: 400}) 69 | rescue Excon::Error 70 | true 71 | end 72 | end 73 | 74 | 75 | tests('can raise with status_error() and catch with legacy error').returns(true) do 76 | begin 77 | raise Excon::Error.status_error({expects: 200}, {status: 400}) 78 | rescue Excon::Errors::BadRequest 79 | true 80 | end 81 | end 82 | 83 | tests('can raise with legacy status_error() and catch with legacy').returns(true) do 84 | begin 85 | raise Excon::Errors.status_error({expects: 200}, {status: 400}) 86 | rescue Excon::Errors::BadRequest 87 | true 88 | end 89 | end 90 | 91 | 92 | tests('can raise with legacy status_error() and catch with standard').returns(true) do 93 | begin 94 | raise Excon::Errors.status_error({expects: 200}, {status: 400}) 95 | rescue Excon::Error 96 | true 97 | end 98 | end 99 | 100 | with_server('error') do 101 | 102 | tests('message does not include response or response info').returns(true) do 103 | begin 104 | Excon.get('http://127.0.0.1:9292/error/not_found', :expects => 200) 105 | rescue Excon::Errors::HTTPStatusError => err 106 | err.message.include?('Expected(200) <=> Actual(404 Not Found)') && 107 | !err.message.include?('excon.error.request') && 108 | !err.message.include?('excon.error.response') 109 | end 110 | end 111 | 112 | tests('message includes only request info').returns(true) do 113 | begin 114 | Excon.get('http://127.0.0.1:9292/error/not_found', :expects => 200, 115 | :debug_request => true) 116 | rescue Excon::Errors::HTTPStatusError => err 117 | err.message.include?('Expected(200) <=> Actual(404 Not Found)') && 118 | err.message.include?('excon.error.request') && 119 | !err.message.include?('excon.error.response') 120 | end 121 | end 122 | 123 | tests('message includes only response info').returns(true) do 124 | begin 125 | Excon.get('http://127.0.0.1:9292/error/not_found', :expects => 200, 126 | :debug_response => true) 127 | rescue Excon::Errors::HTTPStatusError => err 128 | err.message.include?('Expected(200) <=> Actual(404 Not Found)') && 129 | !err.message.include?('excon.error.request') && 130 | err.message.include?('excon.error.response') 131 | end 132 | end 133 | 134 | tests('message include request and response info').returns(true) do 135 | begin 136 | Excon.get('http://127.0.0.1:9292/error/not_found', :expects => 200, 137 | :debug_request => true, :debug_response => true) 138 | rescue Excon::Errors::HTTPStatusError => err 139 | err.message.include?('Expected(200) <=> Actual(404 Not Found)') && 140 | err.message.include?('excon.error.request') && 141 | err.message.include?('excon.error.response') 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /tests/middlewares/idempotent_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon request idempotencey') do 2 | 3 | before do 4 | @connection = Excon.new('http://127.0.0.1:9292', :mock => true) 5 | end 6 | 7 | after do 8 | # flush any existing stubs after each test 9 | Excon.stubs.clear 10 | end 11 | 12 | tests("Non-idempotent call with an erroring socket").raises(Excon::Errors::SocketError) do 13 | run_count = 0 14 | Excon.stub({:method => :get}) { |params| 15 | run_count += 1 16 | if run_count <= 3 # First 3 calls fail. 17 | raise Excon::Errors::SocketError.new(Exception.new "Mock Error") 18 | else 19 | {:body => params[:body], :headers => params[:headers], :status => 200} 20 | end 21 | } 22 | 23 | @connection.request(:method => :get, :path => '/some-path') 24 | end 25 | 26 | tests("Idempotent request with socket erroring first 3 times").returns(200) do 27 | run_count = 0 28 | Excon.stub({:method => :get}) { |params| 29 | run_count += 1 30 | if run_count <= 3 # First 3 calls fail. 31 | raise Excon::Errors::SocketError.new(Exception.new "Mock Error") 32 | else 33 | {:body => params[:body], :headers => params[:headers], :status => 200} 34 | end 35 | } 36 | 37 | response = @connection.request(:method => :get, :idempotent => true, :path => '/some-path') 38 | response.status 39 | end 40 | 41 | tests("Idempotent request with socket erroring first 5 times").raises(Excon::Errors::SocketError) do 42 | run_count = 0 43 | Excon.stub({:method => :get}) { |params| 44 | run_count += 1 45 | if run_count <= 5 # First 5 calls fail. 46 | raise Excon::Errors::SocketError.new(Exception.new "Mock Error") 47 | else 48 | {:body => params[:body], :headers => params[:headers], :status => 200} 49 | end 50 | } 51 | 52 | response = @connection.request(:method => :get, :idempotent => true, :path => '/some-path') 53 | response.status 54 | end 55 | 56 | tests("Lowered retry limit with socket erroring first time").returns(200) do 57 | run_count = 0 58 | Excon.stub({:method => :get}) { |params| 59 | run_count += 1 60 | if run_count <= 1 # First call fails. 61 | raise Excon::Errors::SocketError.new(Exception.new "Mock Error") 62 | else 63 | {:body => params[:body], :headers => params[:headers], :status => 200} 64 | end 65 | } 66 | 67 | response = @connection.request(:method => :get, :idempotent => true, :path => '/some-path', :retry_limit => 2) 68 | response.status 69 | end 70 | 71 | tests("Lowered retry limit with socket erroring first 3 times").raises(Excon::Errors::SocketError) do 72 | run_count = 0 73 | Excon.stub({:method => :get}) { |params| 74 | run_count += 1 75 | if run_count <= 3 # First 3 calls fail. 76 | raise Excon::Errors::SocketError.new(Exception.new "Mock Error") 77 | else 78 | {:body => params[:body], :headers => params[:headers], :status => 200} 79 | end 80 | } 81 | 82 | response = @connection.request(:method => :get, :idempotent => true, :path => '/some-path', :retry_limit => 2) 83 | response.status 84 | end 85 | 86 | tests("Raised retry limit with socket erroring first 5 times").returns(200) do 87 | run_count = 0 88 | Excon.stub({:method => :get}) { |params| 89 | run_count += 1 90 | if run_count <= 5 # First 5 calls fail. 91 | raise Excon::Errors::SocketError.new(Exception.new "Mock Error") 92 | else 93 | {:body => params[:body], :headers => params[:headers], :status => 200} 94 | end 95 | } 96 | 97 | response = @connection.request(:method => :get, :idempotent => true, :path => '/some-path', :retry_limit => 8) 98 | response.status 99 | end 100 | 101 | tests("Raised retry limit with socket erroring first 9 times").raises(Excon::Errors::SocketError) do 102 | run_count = 0 103 | Excon.stub({:method => :get}) { |params| 104 | run_count += 1 105 | if run_count <= 9 # First 9 calls fail. 106 | raise Excon::Errors::SocketError.new(Exception.new "Mock Error") 107 | else 108 | {:body => params[:body], :headers => params[:headers], :status => 200} 109 | end 110 | } 111 | 112 | response = @connection.request(:method => :get, :idempotent => true, :path => '/some-path', :retry_limit => 8) 113 | response.status 114 | end 115 | 116 | tests("Retry limit in constructor with socket erroring first 5 times").returns(200) do 117 | run_count = 0 118 | Excon.stub({:method => :get}) { |params| 119 | run_count += 1 120 | if run_count <= 5 # First 5 calls fail. 121 | raise Excon::Errors::SocketError.new(Exception.new "Mock Error") 122 | else 123 | {:body => params[:body], :headers => params[:headers], :status => 200} 124 | end 125 | } 126 | 127 | response = @connection.request(:method => :get, :idempotent => true, :path => '/some-path', :retry_limit => 6) 128 | response.status 129 | end 130 | 131 | end 132 | -------------------------------------------------------------------------------- /tests/middlewares/decompress_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon Decompress Middleware') do 2 | env_init 3 | 4 | with_server('good') do 5 | 6 | before do 7 | @connection ||= Excon.new( 8 | 'http://127.0.0.1:9292/echo/content-encoded', 9 | :method => :post, 10 | :body => 'hello world', 11 | :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress] 12 | ) 13 | end 14 | 15 | tests('gzip') do 16 | resp = nil 17 | 18 | tests('response body decompressed').returns('hello world') do 19 | resp = @connection.request( 20 | :headers => { 'Accept-Encoding' => 'gzip, deflate;q=0' } 21 | ) 22 | resp[:body] 23 | end 24 | 25 | tests('server sent content-encoding').returns('gzip') do 26 | resp[:headers]['Content-Encoding-Sent'] 27 | end 28 | 29 | tests('removes processed encoding from header').returns('') do 30 | resp[:headers]['Content-Encoding'] 31 | end 32 | 33 | tests('empty response body').returns('') do 34 | resp = @connection.request(:body => '') 35 | resp[:body] 36 | end 37 | end 38 | 39 | tests('deflate') do 40 | resp = nil 41 | 42 | tests('response body decompressed').returns('hello world') do 43 | resp = @connection.request( 44 | :headers => { 'Accept-Encoding' => 'gzip;q=0, deflate' } 45 | ) 46 | resp[:body] 47 | end 48 | 49 | tests('server sent content-encoding').returns('deflate') do 50 | resp[:headers]['Content-Encoding-Sent'] 51 | end 52 | 53 | tests('removes processed encoding from header').returns('') do 54 | resp[:headers]['Content-Encoding'] 55 | end 56 | end 57 | 58 | tests('with pre-encoding') do 59 | resp = nil 60 | 61 | tests('server sent content-encoding').returns('other, gzip') do 62 | resp = @connection.request( 63 | :headers => { 'Accept-Encoding' => 'gzip, deflate;q=0', 64 | 'Content-Encoding-Pre' => 'other' } 65 | ) 66 | resp[:headers]['Content-Encoding-Sent'] 67 | end 68 | 69 | tests('processed encoding removed from header').returns('other') do 70 | resp[:headers]['Content-Encoding'] 71 | end 72 | 73 | tests('response body decompressed').returns('hello world') do 74 | resp[:body] 75 | end 76 | 77 | end 78 | 79 | tests('with post-encoding') do 80 | resp = nil 81 | 82 | tests('server sent content-encoding').returns('gzip, other') do 83 | resp = @connection.request( 84 | :headers => { 'Accept-Encoding' => 'gzip, deflate;q=0', 85 | 'Content-Encoding-Post' => 'other' } 86 | ) 87 | resp[:headers]['Content-Encoding-Sent'] 88 | end 89 | 90 | tests('unprocessed since last applied is unknown').returns('gzip, other') do 91 | resp[:headers]['Content-Encoding'] 92 | end 93 | 94 | tests('response body still compressed').returns('hello world') do 95 | Zlib::GzipReader.new(StringIO.new(resp[:body])).read 96 | end 97 | 98 | end 99 | 100 | tests('with a :response_block') do 101 | captures = nil 102 | resp = nil 103 | 104 | tests('server sent content-encoding').returns('gzip') do 105 | captures = capture_response_block do |block| 106 | resp = @connection.request( 107 | :headers => { 'Accept-Encoding' => 'gzip'}, 108 | :response_block => block 109 | ) 110 | end 111 | resp[:headers]['Content-Encoding-Sent'] 112 | end 113 | 114 | tests('unprocessed since :response_block was used').returns('gzip') do 115 | resp[:headers]['Content-Encoding'] 116 | end 117 | 118 | tests(':response_block passed unprocessed data').returns('hello world') do 119 | body = captures.map {|capture| capture[0] }.join 120 | Zlib::GzipReader.new(StringIO.new(body)).read 121 | end 122 | 123 | end 124 | 125 | tests('adds Accept-Encoding if needed') do 126 | 127 | tests('without a :response_block').returns('deflate, gzip') do 128 | resp = Excon.post( 129 | 'http://127.0.0.1:9292/echo/request', 130 | :body => 'hello world', 131 | :middlewares => Excon.defaults[:middlewares] + 132 | [Excon::Middleware::Decompress] 133 | ) 134 | request = Marshal.load(resp.body) 135 | request[:headers]['Accept-Encoding'] 136 | end 137 | 138 | tests('with a :response_block').returns(nil) do 139 | captures = capture_response_block do |block| 140 | Excon.post( 141 | 'http://127.0.0.1:9292/echo/request', 142 | :body => 'hello world', 143 | :response_block => block, 144 | :middlewares => Excon.defaults[:middlewares] + 145 | [Excon::Middleware::Decompress] 146 | ) 147 | end 148 | request = Marshal.load(captures.map {|capture| capture[0] }.join) 149 | request[:headers]['Accept-Encoding'] 150 | end 151 | 152 | end 153 | 154 | end 155 | 156 | env_restore 157 | end 158 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'date' 4 | include Rake::DSL 5 | ############################################################################# 6 | # 7 | # Helper functions 8 | # 9 | ############################################################################# 10 | 11 | def name 12 | @name ||= Dir['*.gemspec'].first.split('.').first 13 | end 14 | 15 | def version 16 | line = File.read("lib/#{name}/constants.rb")[/^\s*VERSION\s*=\s*.*/] 17 | line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1] 18 | end 19 | 20 | def date 21 | Date.today.to_s 22 | end 23 | 24 | def rubyforge_project 25 | name 26 | end 27 | 28 | def gemspec_file 29 | "#{name}.gemspec" 30 | end 31 | 32 | def gem_file 33 | "#{name}-#{version}.gem" 34 | end 35 | 36 | def replace_header(head, header_name) 37 | head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"} 38 | end 39 | 40 | ############################################################################# 41 | # 42 | # Standard tasks 43 | # 44 | ############################################################################# 45 | 46 | require 'shindo/rake' 47 | require "rspec/core/rake_task" 48 | 49 | RSpec::Core::RakeTask.new(:spec, :format) do |t, args| 50 | format = args[:format] || 'doc' 51 | t.rspec_opts = ["-c", "-f #{format}", "-r ./spec/spec_helper.rb"] 52 | t.pattern = 'spec/**/*_spec.rb' 53 | end 54 | 55 | 56 | Shindo::Rake.new 57 | 58 | task :default => [ :tests, :test] 59 | task :test => :spec 60 | 61 | require 'rdoc/task' 62 | Rake::RDocTask.new do |rdoc| 63 | rdoc.rdoc_dir = 'rdoc' 64 | rdoc.title = "#{name} #{version}" 65 | rdoc.rdoc_files.include('README*') 66 | rdoc.rdoc_files.include('lib/**/*.rb') 67 | end 68 | 69 | desc "Open an irb session preloaded with this library" 70 | task :console do 71 | sh "irb -rubygems -r ./lib/#{name}.rb" 72 | end 73 | 74 | ############################################################################# 75 | # 76 | # Custom tasks (add your own tasks here) 77 | # 78 | ############################################################################# 79 | 80 | 81 | 82 | ############################################################################# 83 | # 84 | # Packaging tasks 85 | # 86 | ############################################################################# 87 | 88 | task :release => [:update_certs, :build] do 89 | unless `git branch` =~ /^\* master$/ 90 | puts "You must be on the master branch to release!" 91 | exit! 92 | end 93 | sh "gem install pkg/#{name}-#{version}.gem" 94 | sh "git commit --allow-empty -a -m 'Release #{version}'" 95 | sh "git tag v#{version}" 96 | sh "git push origin master" 97 | sh "git push origin v#{version}" 98 | sh "gem push pkg/#{name}-#{version}.gem" 99 | end 100 | 101 | task :build => :gemspec do 102 | sh "mkdir -p pkg" 103 | sh "gem build #{gemspec_file}" 104 | sh "mv #{gem_file} pkg" 105 | end 106 | 107 | task :gemspec => :validate do 108 | # read spec file and split out manifest section 109 | spec = File.read(gemspec_file) 110 | head, manifest, tail = spec.split(" # = MANIFEST =\n") 111 | 112 | # replace name version and date 113 | replace_header(head, :name) 114 | replace_header(head, :version) 115 | replace_header(head, :date) 116 | #comment this out if your rubyforge_project has a different name 117 | replace_header(head, :rubyforge_project) 118 | 119 | # determine file list from git ls-files 120 | files = `git ls-files`. 121 | split("\n"). 122 | sort. 123 | reject { |file| file =~ /^\./ }. 124 | reject { |file| file =~ /^(rdoc|pkg)/ }. 125 | map { |file| " #{file}" }. 126 | join("\n") 127 | 128 | # piece file back together and write 129 | manifest = " s.files = %w[\n#{files}\n ]\n" 130 | spec = [head, manifest, tail].join(" # = MANIFEST =\n") 131 | File.open(gemspec_file, 'w') { |io| io.write(spec) } 132 | puts "Updated #{gemspec_file}" 133 | end 134 | 135 | task :validate do 136 | libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"] 137 | unless libfiles.empty? 138 | puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir." 139 | exit! 140 | end 141 | unless Dir['VERSION*'].empty? 142 | puts "A `VERSION` file at root level violates Gem best practices." 143 | exit! 144 | end 145 | end 146 | 147 | desc "update bundled certs" 148 | task :update_certs do 149 | # update curl bundle for end-users 150 | require File.join(File.dirname(__FILE__), 'lib', 'excon') 151 | File.open(File.join(File.dirname(__FILE__), 'data', 'cacert.pem'), 'w') do |file| 152 | data = Excon.get("https://curl.haxx.se/ca/cacert.pem").body 153 | file.write(data) 154 | end 155 | 156 | # update self-signed certs for tests 157 | sh "openssl req -subj '/CN=excon/O=excon' -new -newkey rsa:2048 -sha256 -days 365 -nodes -x509 -keyout tests/data/excon.cert.key -out tests/data/excon.cert.crt" 158 | sh "openssl req -subj '/CN=127.0.0.1/O=excon' -new -newkey rsa:2048 -sha256 -days 365 -nodes -x509 -keyout tests/data/127.0.0.1.cert.key -out tests/data/127.0.0.1.cert.crt" 159 | end 160 | 161 | 162 | desc "check ssl settings" 163 | task :hows_my_ssl do 164 | require File.join(File.dirname(__FILE__), 'lib', 'excon') 165 | data = Excon.get("https://www.howsmyssl.com/a/check").body 166 | puts data 167 | end 168 | -------------------------------------------------------------------------------- /spec/excon/error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Excon::Error do 4 | # Regression against e300458f2d9330cb265baeb8973120d08c665d9 5 | describe '#status_errors' do 6 | describe '.keys ' do 7 | expected = [ 8 | 100, 9 | 101, 10 | (200..206).to_a, 11 | (300..307).to_a, 12 | (400..417).to_a, 13 | 422, 14 | 429, 15 | (500..504).to_a 16 | ].flatten 17 | 18 | it('returns the pertinent HTTP error numbers') do 19 | expected.flatten == Excon::Error.status_errors.keys 20 | end 21 | end 22 | end 23 | 24 | describe '#new' do 25 | it('returns an Excon::Error') do 26 | expect(Excon::Error.new('bar').class == Excon::Error).to be true 27 | end 28 | it('raises errors for bad URIs') do 29 | expect { Excon.new('foo') }.to raise_error(ArgumentError) 30 | end 31 | 32 | it('raises errors for bad paths') do 33 | expect { Excon.new('http://localhost', path: "foo\r\nbar: baz") }.to raise_error(URI::InvalidURIError) 34 | end 35 | end 36 | 37 | context 'when remaining backwards compatible' do 38 | describe '#new' do 39 | it 'should raise standard error and catch standard error' do 40 | expect { raise Excon::Error::Client, 'foo' }.to raise_error(Excon::Error) 41 | end 42 | 43 | it 'should raise legacy errors and catch legacy errors' do 44 | expect do 45 | raise Excon::Errors::Error, 'bar' 46 | end.to raise_error(Excon::Errors::Error) 47 | end 48 | 49 | it 'should raise standard error and catch legacy errors' do 50 | expect do 51 | raise Excon::Error::NotFound, 'bar' 52 | end.to raise_error(Excon::Errors::Error) 53 | end 54 | end 55 | 56 | describe '#status_error' do 57 | it 'should raise with status_error() and catch with standard error' do 58 | expect do 59 | raise Excon::Error.status_error({ expects: 200 }, status: 400) 60 | end.to raise_error(Excon::Error) 61 | end 62 | 63 | it 'should raise with status_error() and catch with legacy error' do 64 | expect do 65 | raise Excon::Error.status_error({ expects: 200 }, status: 400) 66 | end.to raise_error(Excon::Errors::BadRequest) 67 | end 68 | 69 | it 'should raise with legacy status_error() and catch with standard' do 70 | expect do 71 | raise Excon::Errors.status_error({ expects: 200 }, status: 400) 72 | end.to raise_error(Excon::Error) 73 | end 74 | end 75 | end 76 | 77 | context 'when exceptions are rescued' do 78 | include_context("test server", :exec, 'error.rb', :before => :start, :after => :stop ) 79 | 80 | context 'when :debug_request and :debug_response are switched off' do 81 | it('exception message does not include response or response info') do 82 | begin 83 | Excon.get('http://127.0.0.1:9292/error/not_found', expects: 200) 84 | rescue Excon::Errors::HTTPStatusError => err 85 | truth = 86 | err.message.include?('Expected(200) <=> Actual(404 Not Found)') && 87 | !err.message.include?('excon.error.request') && 88 | !err.message.include?('excon.error.response') 89 | expect(truth).to be true 90 | end 91 | end 92 | end 93 | 94 | context 'when :debug_request and :debug_response are switched on' do 95 | it 'exception message includes request and response info' do 96 | begin 97 | Excon.get('http://127.0.0.1:9292/error/not_found', expects: 200, 98 | debug_request: true, debug_response: true) 99 | rescue Excon::Errors::HTTPStatusError => err 100 | truth = 101 | err.message.include?('Expected(200) <=> Actual(404 Not Found)') && 102 | err.message.include?('excon.error.request') && 103 | err.message.include?('excon.error.response') 104 | expect(truth).to be true 105 | end 106 | end 107 | end 108 | 109 | context 'when only :debug_request is turned on' do 110 | it('exception message includes only request info') do 111 | begin 112 | Excon.get('http://127.0.0.1:9292/error/not_found', expects: 200, 113 | debug_request: true) 114 | rescue Excon::Errors::HTTPStatusError => err 115 | truth = 116 | err.message.include?('Expected(200) <=> Actual(404 Not Found)') && 117 | err.message.include?('excon.error.request') && 118 | !err.message.include?('excon.error.response') 119 | expect(truth).to be true 120 | end 121 | end 122 | end 123 | 124 | context 'when only :debug_response is turned on ' do 125 | it('exception message includes only response info') do 126 | begin 127 | Excon.get('http://127.0.0.1:9292/error/not_found', expects: 200, 128 | debug_response: true) 129 | rescue Excon::Errors::HTTPStatusError => err 130 | truth = 131 | err.message.include?('Expected(200) <=> Actual(404 Not Found)') && 132 | !err.message.include?('excon.error.request') && 133 | err.message.include?('excon.error.response') 134 | expect(truth).to be true 135 | end 136 | end 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | * Aaron Stone 2 | * Adam Esterline 3 | * Alexander Sandström 4 | * Andrew Katz 5 | * Andy Delcambre 6 | * Anshul Khandelwal 7 | * Ash Wilson 8 | * Ben Burkert 9 | * Benedikt Böhm 10 | * Bo Jeanes 11 | * Brandur 12 | * Brian D. Burns 13 | * Brian Hartsock 14 | * Bryan Paxton 15 | * Caio Chassot 16 | * Caius Durling 17 | * Carl Hörberg 18 | * Carl Hörberg 19 | * Carlos Sanchez 20 | * Casper Thomsen 21 | * Chris Hanks 22 | * Claudio Poli 23 | * Damien Mathieu 24 | * Dan Hensgen 25 | * Dan Peterson 26 | * Dan Prince 27 | * Dane Harrigan 28 | * Dave Myron 29 | * Dave Newton 30 | * David Biehl 31 | * David Biehl 32 | * Dimitrij Denissenko 33 | * Dominik Richter 34 | * Doug McInnes 35 | * Eugene Howe 36 | * Evan Phoenix 37 | * Fabian Wiesel 38 | * Federico Ravasio 39 | * Glenn Pratt 40 | * Graeme Nelson 41 | * Guillaume Balaine 42 | * Hakan Ensari 43 | * Ian Neubert 44 | * Jacob Atzen 45 | * James Cox 46 | * James Watling 47 | * Jean Mertz 48 | * Jeremy Hinegardner 49 | * Jesse Kempf 50 | * Joe Rafaniello 51 | * John Keiser 52 | * John Leach 53 | * Jonas Pfenniger 54 | * Jonathan Dance 55 | * Jonathan Dance 56 | * Jonathan Roes 57 | * Joshua B. Smith 58 | * Joshua Gross 59 | * Joshua Mckinney 60 | * Joshua Napoli 61 | * Joshua Napoli 62 | * Kelly Mahan 63 | * Kensuke Nagae 64 | * Konstantin Shabanov 65 | * Kyle Rames 66 | * Lewis Marshall 67 | * Lincoln Stoll 68 | * Louis Sobel 69 | * Mahemoff 70 | * Mathias Meyer 71 | * Matt Gauger 72 | * Matt Sanders 73 | * Matt Sanders 74 | * Matt Snyder 75 | * Matt Todd 76 | * Max Lincoln 77 | * Michael Brodhead 78 | * Michael Hale 79 | * Michael Rowe 80 | * Michael Rykov 81 | * Mike Heffner 82 | * Myron Marston 83 | * Nathan Long 84 | * Nathan Sutton 85 | * Nick Osborn 86 | * Nicolas Sanguinetti 87 | * Paul Gideon Dann 88 | * Pavel 89 | * Peter Meier 90 | * Peter Weldon 91 | * Peter Weldon 92 | * Phil Ross 93 | * Richard Ramsden 94 | * Ruslan Korolev 95 | * Ruslan Korolev 96 | * Ruslan Kyrychuk 97 | * Ryan Bigg 98 | * Ryan Mohr 99 | * Sam Withrow 100 | * Scott Gonyea 101 | * Scott Gonyea 102 | * Scott Walkinshaw 103 | * Sean Cribbs 104 | * Sergio Rubio 105 | * Shai Rosenfeld 106 | * Stefan Merettig 107 | * Stephen Chu 108 | * Swanand Pagnis 109 | * Terry Howe 110 | * Thom Mahoney & Josh Lane 111 | * Thom May 112 | * Tim Carey-Smith 113 | * Todd Lunter 114 | * Tom Maher 115 | * Tom Maher 116 | * Trym Skaar 117 | * Tuomas Silen 118 | * Victor Costan 119 | * Viven 120 | * Wesley Beary 121 | * Wesley Beary 122 | * Wesley Beary 123 | * Wesley Beary 124 | * Wesley Beary 125 | * Zach Anker 126 | * chrisrhoden 127 | * dickeyxxx 128 | * geemus (Wesley Beary) 129 | * geemus 130 | * ggoodale 131 | * marios 132 | * mkb 133 | * phiggins 134 | * rin_ne 135 | * rinrinne 136 | * rkyrychuk 137 | * sshaw 138 | * starbelly 139 | * twrodriguez 140 | * zimbatm -------------------------------------------------------------------------------- /lib/excon/ssl_socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | class SSLSocket < Socket 4 | HAVE_NONBLOCK = [:connect_nonblock, :read_nonblock, :write_nonblock].all? do |m| 5 | OpenSSL::SSL::SSLSocket.public_method_defined?(m) 6 | end 7 | 8 | def initialize(data = {}) 9 | super 10 | 11 | # create ssl context 12 | ssl_context = OpenSSL::SSL::SSLContext.new 13 | 14 | # disable less secure options, when supported 15 | ssl_context_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] 16 | if defined?(OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS) 17 | ssl_context_options &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS 18 | end 19 | 20 | if defined?(OpenSSL::SSL::OP_NO_COMPRESSION) 21 | ssl_context_options |= OpenSSL::SSL::OP_NO_COMPRESSION 22 | end 23 | ssl_context.options = ssl_context_options 24 | 25 | ssl_context.ciphers = @data[:ciphers] 26 | if @data[:ssl_version] 27 | ssl_context.ssl_version = @data[:ssl_version] 28 | end 29 | 30 | if @data[:ssl_verify_peer] 31 | # turn verification on 32 | ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER 33 | 34 | if ca_file = @data[:ssl_ca_file] || ENV['SSL_CERT_FILE'] 35 | ssl_context.ca_file = ca_file 36 | end 37 | if ca_path = @data[:ssl_ca_path] || ENV['SSL_CERT_DIR'] 38 | ssl_context.ca_path = ca_path 39 | end 40 | if cert_store = @data[:ssl_cert_store] 41 | ssl_context.cert_store = cert_store 42 | end 43 | 44 | # no defaults, fallback to bundled 45 | unless ca_file || ca_path || cert_store 46 | ssl_context.cert_store = OpenSSL::X509::Store.new 47 | ssl_context.cert_store.set_default_paths 48 | 49 | # workaround issue #257 (JRUBY-6970) 50 | ca_file = DEFAULT_CA_FILE 51 | ca_file = ca_file.gsub(/^jar:/, '') if ca_file =~ /^jar:file:\// 52 | 53 | begin 54 | ssl_context.cert_store.add_file(ca_file) 55 | rescue 56 | Excon.display_warning("Excon unable to add file to cert store, ignoring: #{ca_file}\n[#{$!.class}] #{$!.message}") 57 | end 58 | end 59 | 60 | if verify_callback = @data[:ssl_verify_callback] 61 | ssl_context.verify_callback = verify_callback 62 | end 63 | else 64 | # turn verification off 65 | ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE 66 | end 67 | 68 | if client_cert_data && client_key_data 69 | ssl_context.cert = OpenSSL::X509::Certificate.new client_cert_data 70 | if OpenSSL::PKey.respond_to? :read 71 | ssl_context.key = OpenSSL::PKey.read(client_key_data, client_key_pass) 72 | else 73 | ssl_context.key = OpenSSL::PKey::RSA.new(client_key_data, client_key_pass) 74 | end 75 | elsif @data.key?(:certificate) && @data.key?(:private_key) 76 | ssl_context.cert = OpenSSL::X509::Certificate.new(@data[:certificate]) 77 | if OpenSSL::PKey.respond_to? :read 78 | ssl_context.key = OpenSSL::PKey.read(@data[:private_key], client_key_pass) 79 | else 80 | ssl_context.key = OpenSSL::PKey::RSA.new(@data[:private_key], client_key_pass) 81 | end 82 | end 83 | 84 | if @data[:proxy] 85 | request = "CONNECT #{@data[:host]}#{port_string(@data.merge(:omit_default_port => false))}#{Excon::HTTP_1_1}" + 86 | "Host: #{@data[:host]}#{port_string(@data)}#{Excon::CR_NL}" 87 | 88 | if @data[:proxy].has_key?(:user) || @data[:proxy].has_key?(:password) 89 | user, pass = Utils.unescape_form(@data[:proxy][:user].to_s), Utils.unescape_form(@data[:proxy][:password].to_s) 90 | auth = ["#{user}:#{pass}"].pack('m').delete(Excon::CR_NL) 91 | request += "Proxy-Authorization: Basic #{auth}#{Excon::CR_NL}" 92 | end 93 | 94 | request += "Proxy-Connection: Keep-Alive#{Excon::CR_NL}" 95 | 96 | request += Excon::CR_NL 97 | 98 | # write out the proxy setup request 99 | @socket.write(request) 100 | 101 | # eat the proxy's connection response 102 | Excon::Response.parse(self, :expects => 200, :method => 'CONNECT') 103 | end 104 | 105 | # convert Socket to OpenSSL::SSL::SSLSocket 106 | @socket = OpenSSL::SSL::SSLSocket.new(@socket, ssl_context) 107 | @socket.sync_close = true 108 | 109 | # Server Name Indication (SNI) RFC 3546 110 | if @socket.respond_to?(:hostname=) 111 | @socket.hostname = @data[:host] 112 | end 113 | 114 | begin 115 | if @nonblock 116 | begin 117 | @socket.connect_nonblock 118 | rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable 119 | select_with_timeout(@socket, :connect_read) && retry 120 | rescue IO::WaitWritable 121 | select_with_timeout(@socket, :connect_write) && retry 122 | end 123 | else 124 | @socket.connect 125 | end 126 | rescue Errno::ETIMEDOUT, Timeout::Error 127 | raise Excon::Errors::Timeout.new('connect timeout reached') 128 | end 129 | 130 | # verify connection 131 | if @data[:ssl_verify_peer] 132 | @socket.post_connection_check(@data[:ssl_verify_peer_host] || @data[:host]) 133 | end 134 | 135 | @socket 136 | end 137 | 138 | private 139 | 140 | def client_cert_data 141 | @client_cert_data ||= if ccd = @data[:client_cert_data] 142 | ccd 143 | elsif path = @data[:client_cert] 144 | File.read path 145 | elsif path = @data[:certificate_path] 146 | warn ":certificate_path is no longer supported and will be deprecated. Please use :client_cert or :client_cert_data" 147 | File.read path 148 | end 149 | end 150 | 151 | def connect 152 | # backwards compatability for things lacking nonblock 153 | @nonblock = HAVE_NONBLOCK && @nonblock 154 | super 155 | end 156 | 157 | def client_key_data 158 | @client_key_data ||= if ckd = @data[:client_key_data] 159 | ckd 160 | elsif path = @data[:client_key] 161 | File.read path 162 | elsif path = @data[:private_key_path] 163 | warn ":private_key_path is no longer supported and will be deprecated. Please use :client_key or :client_key_data" 164 | File.read path 165 | end 166 | end 167 | 168 | def client_key_pass 169 | @data[:client_key_pass] || @data[:private_key_pass] 170 | end 171 | 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /tests/response_tests.rb: -------------------------------------------------------------------------------- 1 | Shindo.tests('Excon Response Parsing') do 2 | env_init 3 | 4 | with_server('good') do 5 | 6 | tests('responses with chunked transfer-encoding') do 7 | 8 | tests('simple response').returns('hello world') do 9 | Excon.get('http://127.0.0.1:9292/chunked/simple').body 10 | end 11 | 12 | tests('with :response_block') do 13 | 14 | tests('simple response'). 15 | returns([['hello ', nil, nil], ['world', nil, nil]]) do 16 | capture_response_block do |block| 17 | Excon.get('http://127.0.0.1:9292/chunked/simple', 18 | :response_block => block, 19 | :chunk_size => 5) # not used 20 | end 21 | end 22 | 23 | tests('simple response has empty body').returns('') do 24 | response_block = lambda { |_, _, _| } 25 | Excon.get('http://127.0.0.1:9292/chunked/simple', :response_block => response_block).body 26 | end 27 | 28 | tests('with expected response status'). 29 | returns([['hello ', nil, nil], ['world', nil, nil]]) do 30 | capture_response_block do |block| 31 | Excon.get('http://127.0.0.1:9292/chunked/simple', 32 | :response_block => block, 33 | :expects => 200) 34 | end 35 | end 36 | 37 | tests('with unexpected response status').returns('hello world') do 38 | begin 39 | Excon.get('http://127.0.0.1:9292/chunked/simple', 40 | :response_block => Proc.new { raise 'test failed' }, 41 | :expects => 500) 42 | rescue Excon::Errors::HTTPStatusError => err 43 | err.response[:body] 44 | end 45 | end 46 | 47 | end 48 | 49 | tests('merges trailers into headers'). 50 | returns('one, two, three, four, five, six') do 51 | Excon.get('http://127.0.0.1:9292/chunked/trailers').headers['Test-Header'] 52 | end 53 | 54 | tests("removes 'chunked' from Transfer-Encoding").returns(nil) do 55 | Excon.get('http://127.0.0.1:9292/chunked/simple').headers['Transfer-Encoding'] 56 | end 57 | 58 | end 59 | 60 | tests('responses with content-length') do 61 | 62 | tests('simple response').returns('hello world') do 63 | Excon.get('http://127.0.0.1:9292/content-length/simple').body 64 | end 65 | 66 | tests('with :response_block') do 67 | 68 | tests('simple response'). 69 | returns([['hello', 6, 11], [' worl', 1, 11], ['d', 0, 11]]) do 70 | capture_response_block do |block| 71 | Excon.get('http://127.0.0.1:9292/content-length/simple', 72 | :response_block => block, 73 | :chunk_size => 5) 74 | end 75 | end 76 | 77 | tests('simple response has empty body').returns('') do 78 | response_block = lambda { |_, _, _| } 79 | Excon.get('http://127.0.0.1:9292/content-length/simple', :response_block => response_block).body 80 | end 81 | 82 | tests('with expected response status'). 83 | returns([['hello', 6, 11], [' worl', 1, 11], ['d', 0, 11]]) do 84 | capture_response_block do |block| 85 | Excon.get('http://127.0.0.1:9292/content-length/simple', 86 | :response_block => block, 87 | :chunk_size => 5, 88 | :expects => 200) 89 | end 90 | end 91 | 92 | tests('with unexpected response status').returns('hello world') do 93 | begin 94 | Excon.get('http://127.0.0.1:9292/content-length/simple', 95 | :response_block => Proc.new { raise 'test failed' }, 96 | :chunk_size => 5, 97 | :expects => 500) 98 | rescue Excon::Errors::HTTPStatusError => err 99 | err.response[:body] 100 | end 101 | end 102 | 103 | end 104 | 105 | end 106 | 107 | tests('responses with unknown length') do 108 | 109 | tests('simple response').returns('hello world') do 110 | Excon.get('http://127.0.0.1:9292/unknown/simple').body 111 | end 112 | 113 | tests('with :response_block') do 114 | 115 | tests('simple response'). 116 | returns([['hello', nil, nil], [' worl', nil, nil], ['d', nil, nil]]) do 117 | capture_response_block do |block| 118 | Excon.get('http://127.0.0.1:9292/unknown/simple', 119 | :response_block => block, 120 | :chunk_size => 5) 121 | end 122 | end 123 | 124 | tests('simple response has empty body').returns('') do 125 | response_block = lambda { |_, _, _| } 126 | Excon.get('http://127.0.0.1:9292/unknown/simple', :response_block => response_block).body 127 | end 128 | 129 | tests('with expected response status'). 130 | returns([['hello', nil, nil], [' worl', nil, nil], ['d', nil, nil]]) do 131 | capture_response_block do |block| 132 | Excon.get('http://127.0.0.1:9292/unknown/simple', 133 | :response_block => block, 134 | :chunk_size => 5, 135 | :expects => 200) 136 | end 137 | end 138 | 139 | tests('with unexpected response status').returns('hello world') do 140 | begin 141 | Excon.get('http://127.0.0.1:9292/unknown/simple', 142 | :response_block => Proc.new { raise 'test failed' }, 143 | :chunk_size => 5, 144 | :expects => 500) 145 | rescue Excon::Errors::HTTPStatusError => err 146 | err.response[:body] 147 | end 148 | end 149 | 150 | end 151 | 152 | end 153 | 154 | tests('cookies') do 155 | 156 | tests('parses cookies into array').returns(['one, two', 'three, four']) do 157 | resp = Excon.get('http://127.0.0.1:9292/unknown/cookies') 158 | resp[:cookies] 159 | end 160 | 161 | end 162 | 163 | tests('header continuation') do 164 | 165 | tests('proper continuation').returns('one, two, three, four, five, six') do 166 | resp = Excon.get('http://127.0.0.1:9292/unknown/header_continuation') 167 | resp.headers['Test-Header'] 168 | end 169 | 170 | tests('malformed header').raises(Excon::Errors::SocketError) do 171 | Excon.get('http://127.0.0.1:9292/bad/malformed_header') 172 | end 173 | 174 | tests('malformed header continuation').raises(Excon::Errors::SocketError) do 175 | Excon.get('http://127.0.0.1:9292/bad/malformed_header_continuation') 176 | end 177 | 178 | end 179 | 180 | tests('status line parsing') do 181 | 182 | tests('proper status code').returns(404) do 183 | resp = Excon.get('http://127.0.0.1:9292/not-found') 184 | resp.status 185 | end 186 | 187 | tests('proper reason phrase').returns("Not Found") do 188 | resp = Excon.get('http://127.0.0.1:9292/not-found') 189 | resp.reason_phrase 190 | end 191 | 192 | end 193 | 194 | end 195 | 196 | env_restore 197 | end 198 | -------------------------------------------------------------------------------- /benchmarks/downcase-eq-eq_vs_casecmp.rb: -------------------------------------------------------------------------------- 1 | # Copied from my benchmark_hell repo: github.com/sgonyea/benchmark_hell 2 | 3 | require 'benchmark' 4 | 5 | iters = 1000000 6 | 7 | comp = "hello" 8 | hello = "HelLo" 9 | 10 | puts 'String#downcase == vs. String#casecmp' 11 | Benchmark.bmbm do |x| 12 | x.report('String#downcase1') do 13 | iters.times.each do 14 | hello.downcase == comp 15 | end 16 | end 17 | 18 | x.report('String#downcase2') do 19 | iters.times.each do 20 | "HelLo".downcase == "hello" 21 | end 22 | end 23 | 24 | x.report('String#downcase3') do 25 | iters.times.each do 26 | var = "HelLo" 27 | var.downcase! 28 | var == "hello" 29 | end 30 | end 31 | 32 | x.report('casecmp1') do 33 | iters.times.each do 34 | hello.casecmp(comp).zero? 35 | end 36 | end 37 | 38 | x.report('casecmp1-1') do 39 | iters.times.each do 40 | hello.casecmp(comp) == 0 41 | end 42 | end 43 | 44 | x.report('casecmp2') do 45 | iters.times.each do 46 | "HelLo".casecmp(comp).zero? 47 | end 48 | end 49 | 50 | x.report('casecmp2-1') do 51 | iters.times.each do 52 | "HelLo".casecmp(comp) == 0 53 | end 54 | end 55 | end 56 | 57 | =begin 58 | rvm exec bash -c 'echo && echo $RUBY_VERSION && echo && ruby downcase-eq-eq_vs_casecmp.rb' 59 | 60 | jruby-1.5.6 61 | 62 | String#downcase == vs. String#casecmp 63 | Rehearsal ---------------------------------------------------- 64 | String#downcase1 0.461000 0.000000 0.461000 ( 0.387000) 65 | String#downcase2 0.269000 0.000000 0.269000 ( 0.269000) 66 | String#downcase3 0.224000 0.000000 0.224000 ( 0.224000) 67 | casecmp1 0.157000 0.000000 0.157000 ( 0.157000) 68 | casecmp1-1 0.153000 0.000000 0.153000 ( 0.153000) 69 | casecmp2 0.163000 0.000000 0.163000 ( 0.163000) 70 | casecmp2-1 0.163000 0.000000 0.163000 ( 0.163000) 71 | ------------------------------------------- total: 1.590000sec 72 | 73 | user system total real 74 | String#downcase1 0.190000 0.000000 0.190000 ( 0.191000) 75 | String#downcase2 0.225000 0.000000 0.225000 ( 0.225000) 76 | String#downcase3 0.190000 0.000000 0.190000 ( 0.190000) 77 | casecmp1 0.125000 0.000000 0.125000 ( 0.125000) 78 | casecmp1-1 0.127000 0.000000 0.127000 ( 0.127000) 79 | casecmp2 0.144000 0.000000 0.144000 ( 0.144000) 80 | casecmp2-1 0.147000 0.000000 0.147000 ( 0.147000) 81 | 82 | macruby-0.7.1 83 | 84 | String#downcase == vs. String#casecmp 85 | Rehearsal ---------------------------------------------------- 86 | String#downcase1 2.340000 0.040000 2.380000 ( 1.765141) 87 | String#downcase2 5.510000 0.100000 5.610000 ( 3.893249) 88 | String#downcase3 4.200000 0.080000 4.280000 ( 3.031621) 89 | casecmp1 0.270000 0.000000 0.270000 ( 0.267613) 90 | casecmp1-1 0.190000 0.000000 0.190000 ( 0.188848) 91 | casecmp2 1.450000 0.020000 1.470000 ( 1.027956) 92 | casecmp2-1 1.380000 0.030000 1.410000 ( 0.951474) 93 | ------------------------------------------ total: 15.610000sec 94 | 95 | user system total real 96 | String#downcase1 2.350000 0.040000 2.390000 ( 1.774292) 97 | String#downcase2 5.890000 0.120000 6.010000 ( 4.214038) 98 | String#downcase3 4.530000 0.090000 4.620000 ( 3.286059) 99 | casecmp1 0.270000 0.000000 0.270000 ( 0.271119) 100 | casecmp1-1 0.190000 0.000000 0.190000 ( 0.189462) 101 | casecmp2 1.540000 0.030000 1.570000 ( 1.104751) 102 | casecmp2-1 1.440000 0.030000 1.470000 ( 0.999689) 103 | 104 | rbx-head 105 | 106 | String#downcase == vs. String#casecmp 107 | Rehearsal ---------------------------------------------------- 108 | String#downcase1 0.702746 0.005229 0.707975 ( 0.621969) 109 | String#downcase2 0.701429 0.001617 0.703046 ( 0.691833) 110 | String#downcase3 1.042835 0.002952 1.045787 ( 0.953992) 111 | casecmp1 0.654571 0.002239 0.656810 ( 0.480158) 112 | casecmp1-1 0.484706 0.001105 0.485811 ( 0.398601) 113 | casecmp2 0.564140 0.001579 0.565719 ( 0.545332) 114 | casecmp2-1 0.554889 0.001153 0.556042 ( 0.539569) 115 | ------------------------------------------- total: 4.721190sec 116 | 117 | user system total real 118 | String#downcase1 0.491199 0.001081 0.492280 ( 0.493727) 119 | String#downcase2 0.631059 0.001018 0.632077 ( 0.629885) 120 | String#downcase3 0.968867 0.002504 0.971371 ( 0.976734) 121 | casecmp1 0.364496 0.000434 0.364930 ( 0.365262) 122 | casecmp1-1 0.373140 0.000562 0.373702 ( 0.374136) 123 | casecmp2 0.487644 0.001057 0.488701 ( 0.490302) 124 | casecmp2-1 0.469868 0.001178 0.471046 ( 0.472220) 125 | 126 | ruby-1.8.7-p330 127 | 128 | String#downcase == vs. String#casecmp 129 | Rehearsal ---------------------------------------------------- 130 | String#downcase1 0.780000 0.000000 0.780000 ( 0.783979) 131 | String#downcase2 0.950000 0.000000 0.950000 ( 0.954109) 132 | String#downcase3 0.960000 0.000000 0.960000 ( 0.960554) 133 | casecmp1 0.440000 0.000000 0.440000 ( 0.442546) 134 | casecmp1-1 0.490000 0.000000 0.490000 ( 0.487795) 135 | casecmp2 0.530000 0.000000 0.530000 ( 0.535819) 136 | casecmp2-1 0.570000 0.000000 0.570000 ( 0.574653) 137 | ------------------------------------------- total: 4.720000sec 138 | 139 | user system total real 140 | String#downcase1 0.780000 0.000000 0.780000 ( 0.780692) 141 | String#downcase2 0.980000 0.010000 0.990000 ( 0.982925) 142 | String#downcase3 0.960000 0.000000 0.960000 ( 0.961501) 143 | casecmp1 0.440000 0.000000 0.440000 ( 0.444528) 144 | casecmp1-1 0.490000 0.000000 0.490000 ( 0.487437) 145 | casecmp2 0.540000 0.000000 0.540000 ( 0.537686) 146 | casecmp2-1 0.570000 0.000000 0.570000 ( 0.574253) 147 | 148 | ruby-1.9.2-p136 149 | 150 | String#downcase == vs. String#casecmp 151 | Rehearsal ---------------------------------------------------- 152 | String#downcase1 0.750000 0.000000 0.750000 ( 0.750523) 153 | String#downcase2 1.190000 0.000000 1.190000 ( 1.193346) 154 | String#downcase3 1.030000 0.010000 1.040000 ( 1.036435) 155 | casecmp1 0.640000 0.000000 0.640000 ( 0.640327) 156 | casecmp1-1 0.480000 0.000000 0.480000 ( 0.484709) # With all this crap running, some flukes pop out 157 | casecmp2 0.820000 0.000000 0.820000 ( 0.822223) 158 | casecmp2-1 0.660000 0.000000 0.660000 ( 0.664190) 159 | ------------------------------------------- total: 5.580000sec 160 | 161 | user system total real 162 | String#downcase1 0.760000 0.000000 0.760000 ( 0.759816) 163 | String#downcase2 1.150000 0.010000 1.160000 ( 1.150792) 164 | String#downcase3 1.000000 0.000000 1.000000 ( 1.005549) 165 | casecmp1 0.650000 0.000000 0.650000 ( 0.644021) 166 | casecmp1-1 0.490000 0.000000 0.490000 ( 0.494456) 167 | casecmp2 0.820000 0.000000 0.820000 ( 0.817689) 168 | casecmp2-1 0.680000 0.000000 0.680000 ( 0.685121) 169 | =end 170 | -------------------------------------------------------------------------------- /benchmarks/has_key-vs-lookup.rb: -------------------------------------------------------------------------------- 1 | # Copied from my benchmark_hell repo: github.com/sgonyea/benchmark_hell 2 | 3 | require 'benchmark' 4 | 5 | iters = 1000000 6 | hash = { 7 | 'some_key' => 'some_val', 8 | 'nil_key' => nil 9 | } 10 | 11 | puts 'Hash#has_key vs. Hash#[]' 12 | Benchmark.bmbm do |x| 13 | x.report('Hash#has_key') do 14 | iters.times.each do 15 | hash.has_key? 'some_key' 16 | end 17 | end 18 | 19 | x.report('Hash#has_key (if statement)') do 20 | iters.times.each do 21 | if hash.has_key?('other_key') 22 | "hooray!" 23 | end 24 | end 25 | end 26 | 27 | x.report('Hash#has_key (non-existant)') do 28 | iters.times.each do 29 | hash.has_key? 'other_key' 30 | end 31 | end 32 | 33 | x.report('Hash#[]') do 34 | iters.times.each do 35 | hash['some_key'] 36 | end 37 | end 38 | 39 | x.report('Hash#[] (if statement)') do 40 | iters.times.each do 41 | if hash['some_key'] 42 | "hooray!" 43 | end 44 | end 45 | end 46 | 47 | x.report('Hash#[] (non-existant)') do 48 | iters.times.each do 49 | hash['other_key'] 50 | end 51 | end 52 | 53 | x.report('Hash#has_key (if statement) explicit nil check') do 54 | iters.times.each do 55 | if hash.has_key?('nil_key') && !hash['nil_key'].nil? 56 | "hooray!" 57 | end 58 | end 59 | end 60 | 61 | 62 | x.report('Hash#has_key (if statement) implicit nil check') do 63 | iters.times.each do 64 | if hash.has_key?('nil_key') && hash['nil_key'] 65 | "hooray!" 66 | end 67 | end 68 | end 69 | 70 | x.report('Hash#[] (if statement with nil)') do 71 | iters.times.each do 72 | if hash['nil_key'] 73 | "hooray!" 74 | end 75 | end 76 | end 77 | end 78 | 79 | =begin 80 | 81 | $ rvm exec bash -c 'echo $RUBY_VERSION && ruby has_key-vs-hash\[key\].rb' 82 | 83 | jruby-1.5.6 84 | Hash#has_key vs. Hash#[] 85 | Rehearsal --------------------------------------------------------------- 86 | Hash#has_key 0.410000 0.000000 0.410000 ( 0.341000) 87 | Hash#has_key (if statement) 0.145000 0.000000 0.145000 ( 0.145000) 88 | Hash#has_key (non-existant) 0.116000 0.000000 0.116000 ( 0.116000) 89 | Hash#[] 0.189000 0.000000 0.189000 ( 0.189000) 90 | Hash#[] (if statement) 0.176000 0.000000 0.176000 ( 0.176000) 91 | Hash#[] (non-existant) 0.302000 0.000000 0.302000 ( 0.302000) 92 | ------------------------------------------------------ total: 1.338000sec 93 | 94 | user system total real 95 | Hash#has_key 0.128000 0.000000 0.128000 ( 0.128000) 96 | Hash#has_key (if statement) 0.128000 0.000000 0.128000 ( 0.128000) 97 | Hash#has_key (non-existant) 0.153000 0.000000 0.153000 ( 0.153000) 98 | Hash#[] 0.206000 0.000000 0.206000 ( 0.206000) 99 | Hash#[] (if statement) 0.182000 0.000000 0.182000 ( 0.182000) 100 | Hash#[] (non-existant) 0.252000 0.000000 0.252000 ( 0.252000) 101 | 102 | macruby-0.7.1 103 | Hash#has_key vs. Hash#[] 104 | Rehearsal --------------------------------------------------------------- 105 | Hash#has_key 2.530000 0.050000 2.580000 ( 1.917643) 106 | Hash#has_key (if statement) 2.590000 0.050000 2.640000 ( 1.935221) 107 | Hash#has_key (non-existant) 2.580000 0.050000 2.630000 ( 1.964230) 108 | Hash#[] 2.240000 0.040000 2.280000 ( 1.640999) 109 | Hash#[] (if statement) 3.620000 0.070000 3.690000 ( 2.530248) 110 | Hash#[] (non-existant) 2.060000 0.040000 2.100000 ( 1.473487) 111 | ----------------------------------------------------- total: 15.920000sec 112 | 113 | user system total real 114 | Hash#has_key 2.230000 0.030000 2.260000 ( 1.661843) 115 | Hash#has_key (if statement) 2.180000 0.040000 2.220000 ( 1.605644) 116 | Hash#has_key (non-existant) 2.160000 0.040000 2.200000 ( 1.582561) 117 | Hash#[] 2.160000 0.030000 2.190000 ( 1.581448) 118 | Hash#[] (if statement) 3.440000 0.070000 3.510000 ( 2.393421) 119 | Hash#[] (non-existant) 2.330000 0.040000 2.370000 ( 1.699338) 120 | 121 | rbx-head 122 | Hash#has_key vs. Hash#[] 123 | Rehearsal --------------------------------------------------------------- 124 | Hash#has_key 0.660584 0.004932 0.665516 ( 0.508601) 125 | Hash#has_key (if statement) 0.261708 0.000532 0.262240 ( 0.263021) 126 | Hash#has_key (non-existant) 0.265908 0.000827 0.266735 ( 0.259509) 127 | Hash#[] 0.396607 0.001189 0.397796 ( 0.372997) 128 | Hash#[] (if statement) 0.553003 0.001589 0.554592 ( 0.543859) 129 | Hash#[] (non-existant) 0.323748 0.000884 0.324632 ( 0.319055) 130 | ------------------------------------------------------ total: 2.471511sec 131 | 132 | user system total real 133 | Hash#has_key 0.332239 0.000819 0.333058 ( 0.333809) 134 | Hash#has_key (if statement) 0.284344 0.000521 0.284865 ( 0.285330) 135 | Hash#has_key (non-existant) 0.339695 0.001301 0.340996 ( 0.324259) 136 | Hash#[] 0.298555 0.000368 0.298923 ( 0.299557) 137 | Hash#[] (if statement) 0.392755 0.000773 0.393528 ( 0.395473) 138 | Hash#[] (non-existant) 0.277721 0.000464 0.278185 ( 0.278540) 139 | 140 | ruby-1.8.7-p330 141 | Hash#has_key vs. Hash#[] 142 | Rehearsal --------------------------------------------------------------- 143 | Hash#has_key 0.450000 0.000000 0.450000 ( 0.450143) 144 | Hash#has_key (if statement) 0.440000 0.000000 0.440000 ( 0.448278) 145 | Hash#has_key (non-existant) 0.420000 0.000000 0.420000 ( 0.416959) 146 | Hash#[] 0.450000 0.000000 0.450000 ( 0.450727) 147 | Hash#[] (if statement) 0.550000 0.000000 0.550000 ( 0.555043) 148 | Hash#[] (non-existant) 0.530000 0.000000 0.530000 ( 0.527189) 149 | ------------------------------------------------------ total: 2.840000sec 150 | 151 | user system total real 152 | Hash#has_key 0.440000 0.000000 0.440000 ( 0.447746) 153 | Hash#has_key (if statement) 0.450000 0.000000 0.450000 ( 0.450331) 154 | Hash#has_key (non-existant) 0.420000 0.000000 0.420000 ( 0.419157) 155 | Hash#[] 0.450000 0.000000 0.450000 ( 0.454438) 156 | Hash#[] (if statement) 0.570000 0.000000 0.570000 ( 0.563948) 157 | Hash#[] (non-existant) 0.520000 0.000000 0.520000 ( 0.527866) 158 | 159 | ruby-1.9.2-p136 160 | Hash#has_key vs. Hash#[] 161 | Rehearsal --------------------------------------------------------------- 162 | Hash#has_key 0.690000 0.000000 0.690000 ( 0.691657) 163 | Hash#has_key (if statement) 0.630000 0.000000 0.630000 ( 0.638418) 164 | Hash#has_key (non-existant) 0.640000 0.000000 0.640000 ( 0.637510) 165 | Hash#[] 0.580000 0.000000 0.580000 ( 0.584500) 166 | Hash#[] (if statement) 0.840000 0.010000 0.850000 ( 0.837541) 167 | Hash#[] (non-existant) 0.810000 0.000000 0.810000 ( 0.811598) 168 | ------------------------------------------------------ total: 4.200000sec 169 | 170 | user system total real 171 | Hash#has_key 0.690000 0.000000 0.690000 ( 0.694192) 172 | Hash#has_key (if statement) 0.640000 0.000000 0.640000 ( 0.641729) 173 | Hash#has_key (non-existant) 0.630000 0.000000 0.630000 ( 0.634470) 174 | Hash#[] 0.580000 0.000000 0.580000 ( 0.587844) 175 | Hash#[] (if statement) 0.830000 0.000000 0.830000 ( 0.832323) 176 | Hash#[] (non-existant) 0.790000 0.010000 0.800000 ( 0.791689) 177 | =end 178 | -------------------------------------------------------------------------------- /lib/excon/response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Excon 3 | class Response 4 | 5 | attr_accessor :data 6 | 7 | # backwards compatability reader/writers 8 | def body=(new_body) 9 | @data[:body] = new_body 10 | end 11 | def body 12 | @data[:body] 13 | end 14 | def headers=(new_headers) 15 | @data[:headers] = new_headers 16 | end 17 | def headers 18 | @data[:headers] 19 | end 20 | def host 21 | @data[:host] 22 | end 23 | def local_address 24 | @data[:local_address] 25 | end 26 | def local_port 27 | @data[:local_port] 28 | end 29 | def path 30 | @data[:path] 31 | end 32 | def port 33 | @data[:port] 34 | end 35 | def reason_phrase=(new_reason_phrase) 36 | @data[:reason_phrase] = new_reason_phrase 37 | end 38 | def reason_phrase 39 | @data[:reason_phrase] 40 | end 41 | def remote_ip=(new_remote_ip) 42 | @data[:remote_ip] = new_remote_ip 43 | end 44 | def remote_ip 45 | @data[:remote_ip] 46 | end 47 | def status=(new_status) 48 | @data[:status] = new_status 49 | end 50 | def status 51 | @data[:status] 52 | end 53 | def status_line 54 | @data[:status_line] 55 | end 56 | def status_line=(new_status_line) 57 | @data[:status_line] = new_status_line 58 | end 59 | 60 | def self.parse(socket, datum) 61 | # this will discard any trailing lines from the previous response if any. 62 | begin 63 | line = socket.readline 64 | end until status = line[9, 3].to_i 65 | 66 | reason_phrase = line[13..-3] # -3 strips the trailing "\r\n" 67 | 68 | datum[:response] = { 69 | :body => String.new, 70 | :cookies => [], 71 | :host => datum[:host], 72 | :headers => Excon::Headers.new, 73 | :path => datum[:path], 74 | :port => datum[:port], 75 | :status => status, 76 | :status_line => line, 77 | :reason_phrase => reason_phrase 78 | } 79 | 80 | unix_proxy = datum[:proxy] ? datum[:proxy][:scheme] == UNIX : false 81 | unless datum[:scheme] == UNIX || unix_proxy 82 | datum[:response].merge!( 83 | :remote_ip => socket.remote_ip, 84 | :local_port => socket.local_port, 85 | :local_address => socket.local_address 86 | ) 87 | end 88 | 89 | parse_headers(socket, datum) 90 | 91 | unless (['HEAD', 'CONNECT'].include?(datum[:method].to_s.upcase)) || NO_ENTITY.include?(datum[:response][:status]) 92 | 93 | if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Transfer-Encoding') == 0 } 94 | encodings = Utils.split_header_value(datum[:response][:headers][key]) 95 | if (encoding = encodings.last) && encoding.casecmp('chunked') == 0 96 | transfer_encoding_chunked = true 97 | if encodings.length == 1 98 | datum[:response][:headers].delete(key) 99 | else 100 | datum[:response][:headers][key] = encodings[0...-1].join(', ') 101 | end 102 | end 103 | end 104 | 105 | # use :response_block unless :expects would fail 106 | if response_block = datum[:response_block] 107 | if datum[:middlewares].include?(Excon::Middleware::Expects) && datum[:expects] && 108 | !Array(datum[:expects]).include?(datum[:response][:status]) 109 | response_block = nil 110 | end 111 | end 112 | 113 | if transfer_encoding_chunked 114 | if response_block 115 | while (chunk_size = socket.readline.chomp!.to_i(16)) > 0 116 | while chunk_size > 0 117 | chunk = socket.read(chunk_size) || raise(EOFError) 118 | chunk_size -= chunk.bytesize 119 | response_block.call(chunk, nil, nil) 120 | end 121 | new_line_size = 2 # 2 == "\r\n".length 122 | while new_line_size > 0 123 | chunk = socket.read(new_line_size) || raise(EOFError) 124 | new_line_size -= chunk.length 125 | end 126 | end 127 | else 128 | while (chunk_size = socket.readline.chomp!.to_i(16)) > 0 129 | while chunk_size > 0 130 | chunk = socket.read(chunk_size) || raise(EOFError) 131 | chunk_size -= chunk.bytesize 132 | datum[:response][:body] << chunk 133 | end 134 | new_line_size = 2 # 2 == "\r\n".length 135 | while new_line_size > 0 136 | chunk = socket.read(new_line_size) || raise(EOFError) 137 | new_line_size -= chunk.length 138 | end 139 | end 140 | end 141 | parse_headers(socket, datum) # merge trailers into headers 142 | else 143 | if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Length') == 0 } 144 | content_length = datum[:response][:headers][key].to_i 145 | end 146 | 147 | if remaining = content_length 148 | if response_block 149 | while remaining > 0 150 | chunk = socket.read([datum[:chunk_size], remaining].min) || raise(EOFError) 151 | response_block.call(chunk, [remaining - chunk.bytesize, 0].max, content_length) 152 | remaining -= chunk.bytesize 153 | end 154 | else 155 | while remaining > 0 156 | chunk = socket.read([datum[:chunk_size], remaining].min) || raise(EOFError) 157 | datum[:response][:body] << chunk 158 | remaining -= chunk.bytesize 159 | end 160 | end 161 | else 162 | if response_block 163 | while chunk = socket.read(datum[:chunk_size]) 164 | response_block.call(chunk, nil, nil) 165 | end 166 | else 167 | while chunk = socket.read(datum[:chunk_size]) 168 | datum[:response][:body] << chunk 169 | end 170 | end 171 | end 172 | end 173 | end 174 | datum 175 | end 176 | 177 | def self.parse_headers(socket, datum) 178 | last_key = nil 179 | until (data = socket.readline.chomp).empty? 180 | if !data.lstrip!.nil? 181 | raise Excon::Error::ResponseParse, 'malformed header' unless last_key 182 | # append to last_key's last value 183 | datum[:response][:headers][last_key] << ' ' << data.rstrip 184 | else 185 | key, value = data.split(':', 2) 186 | raise Excon::Error::ResponseParse, 'malformed header' unless value 187 | # add key/value or append value to existing values 188 | datum[:response][:headers][key] = ([datum[:response][:headers][key]] << value.strip).compact.join(', ') 189 | if key.casecmp('Set-Cookie') == 0 190 | datum[:response][:cookies] << value.strip 191 | end 192 | last_key = key 193 | end 194 | end 195 | end 196 | 197 | def initialize(params={}) 198 | @data = { 199 | :body => '' 200 | }.merge(params) 201 | @data[:headers] = Excon::Headers.new.merge!(params[:headers] || {}) 202 | 203 | @body = @data[:body] 204 | @headers = @data[:headers] 205 | @status = @data[:status] 206 | @remote_ip = @data[:remote_ip] 207 | @local_port = @data[:local_port] 208 | @local_address = @data[:local_address] 209 | end 210 | 211 | def [](key) 212 | @data[key] 213 | end 214 | 215 | def params 216 | Excon.display_warning('Excon::Response#params is deprecated use Excon::Response#data instead.') 217 | data 218 | end 219 | 220 | def pp 221 | Excon::PrettyPrinter.pp($stdout, @data) 222 | end 223 | 224 | # Retrieve a specific header value. Header names are treated case-insensitively. 225 | # @param [String] name Header name 226 | def get_header(name) 227 | headers[name] 228 | end 229 | 230 | end # class Response 231 | end # module Excon 232 | -------------------------------------------------------------------------------- /excon.gemspec: -------------------------------------------------------------------------------- 1 | ## This is the rakegem gemspec template. Make sure you read and understand 2 | ## all of the comments. Some sections require modification, and others can 3 | ## be deleted if you don't need them. Once you understand the contents of 4 | ## this file, feel free to delete any comments that begin with two hash marks. 5 | ## You can find comprehensive Gem::Specification documentation, at 6 | ## http://docs.rubygems.org/read/chapter/20 7 | Gem::Specification.new do |s| 8 | s.specification_version = 2 if s.respond_to? :specification_version= 9 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 10 | s.rubygems_version = '1.3.5' 11 | 12 | ## Leave these as is they will be modified for you by the rake gemspec task. 13 | ## If your rubyforge_project name is different, then edit it and comment out 14 | ## the sub! line in the Rakefile 15 | s.name = 'excon' 16 | s.version = '0.57.0' 17 | s.date = '2017-06-14' 18 | s.rubyforge_project = 'excon' 19 | 20 | ## Make sure your summary is short. The description may be as long 21 | ## as you like. 22 | s.summary = "speed, persistence, http(s)" 23 | s.description = "EXtended http(s) CONnections" 24 | 25 | ## List the primary authors. If there are a bunch of authors, it's probably 26 | ## better to set the email to an email list or something. If you don't have 27 | ## a custom homepage, consider using your GitHub URL or the like. 28 | s.authors = ["dpiddy (Dan Peterson)", "geemus (Wesley Beary)", "nextmat (Matt Sanders)"] 29 | s.email = 'geemus@gmail.com' 30 | s.homepage = 'https://github.com/excon/excon' 31 | s.license = 'MIT' 32 | 33 | ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as 34 | ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb' 35 | s.require_paths = %w[lib] 36 | 37 | ## This sections is only necessary if you have C extensions. 38 | # s.require_paths << 'ext' 39 | # s.extensions = %w[ext/extconf.rb] 40 | 41 | ## If your gem includes any executables, list them here. 42 | # s.executables = ["name"] 43 | # s.default_executable = 'name' 44 | 45 | ## Specify any RDoc options here. You'll want to add your README and 46 | ## LICENSE files to the extra_rdoc_files list. 47 | s.rdoc_options = ["--charset=UTF-8"] 48 | s.extra_rdoc_files = %w[README.md] 49 | 50 | ## List your runtime dependencies here. Runtime dependencies are those 51 | ## that are needed for an end user to actually USE your code. 52 | # s.add_dependency('DEPNAME', [">= 1.1.0", "< 2.0.0"]) 53 | 54 | ## List your development dependencies here. Development dependencies are 55 | ## those that are only needed during development 56 | # s.add_development_dependency('DEVDEPNAME', [">= 1.1.0", "< 2.0.0"]) 57 | s.add_development_dependency('rspec', '>= 3.5.0') 58 | s.add_development_dependency('activesupport') 59 | s.add_development_dependency('delorean') 60 | s.add_development_dependency('eventmachine', '>= 1.0.4') 61 | s.add_development_dependency('open4') 62 | s.add_development_dependency('rake') 63 | s.add_development_dependency('rdoc') 64 | s.add_development_dependency('shindo') 65 | s.add_development_dependency('sinatra') 66 | s.add_development_dependency('sinatra-contrib') 67 | s.add_development_dependency('json', '>= 1.8.5') 68 | if RUBY_VERSION.to_f >= 1.9 69 | s.add_development_dependency 'puma' 70 | end 71 | ## Leave this section as-is. It will be automatically generated from the 72 | ## contents of your Git repository via the gemspec task. DO NOT REMOVE 73 | ## THE MANIFEST COMMENTS, they are used as delimiters by the task. 74 | # = MANIFEST = 75 | s.files = %w[ 76 | CONTRIBUTING.md 77 | CONTRIBUTORS.md 78 | Gemfile 79 | Gemfile.lock 80 | LICENSE.md 81 | README.md 82 | Rakefile 83 | benchmarks/class_vs_lambda.rb 84 | benchmarks/concat_vs_insert.rb 85 | benchmarks/concat_vs_interpolate.rb 86 | benchmarks/cr_lf.rb 87 | benchmarks/downcase-eq-eq_vs_casecmp.rb 88 | benchmarks/excon.rb 89 | benchmarks/excon_vs.rb 90 | benchmarks/for_vs_array_each.rb 91 | benchmarks/for_vs_hash_each.rb 92 | benchmarks/has_key-vs-lookup.rb 93 | benchmarks/headers_case_sensitivity.rb 94 | benchmarks/headers_split_vs_match.rb 95 | benchmarks/implicit_block-vs-explicit_block.rb 96 | benchmarks/merging.rb 97 | benchmarks/single_vs_double_quotes.rb 98 | benchmarks/string_ranged_index.rb 99 | benchmarks/strip_newline.rb 100 | benchmarks/vs_stdlib.rb 101 | changelog.txt 102 | data/cacert.pem 103 | excon.gemspec 104 | lib/excon.rb 105 | lib/excon/connection.rb 106 | lib/excon/constants.rb 107 | lib/excon/error.rb 108 | lib/excon/extensions/uri.rb 109 | lib/excon/headers.rb 110 | lib/excon/middlewares/base.rb 111 | lib/excon/middlewares/capture_cookies.rb 112 | lib/excon/middlewares/decompress.rb 113 | lib/excon/middlewares/escape_path.rb 114 | lib/excon/middlewares/expects.rb 115 | lib/excon/middlewares/idempotent.rb 116 | lib/excon/middlewares/instrumentor.rb 117 | lib/excon/middlewares/mock.rb 118 | lib/excon/middlewares/redirect_follower.rb 119 | lib/excon/middlewares/response_parser.rb 120 | lib/excon/pretty_printer.rb 121 | lib/excon/response.rb 122 | lib/excon/socket.rb 123 | lib/excon/ssl_socket.rb 124 | lib/excon/standard_instrumentor.rb 125 | lib/excon/test/plugin/server/exec.rb 126 | lib/excon/test/plugin/server/puma.rb 127 | lib/excon/test/plugin/server/unicorn.rb 128 | lib/excon/test/plugin/server/webrick.rb 129 | lib/excon/test/server.rb 130 | lib/excon/unix_socket.rb 131 | lib/excon/utils.rb 132 | spec/excon/error_spec.rb 133 | spec/excon/test/server_spec.rb 134 | spec/excon_spec.rb 135 | spec/helpers/file_path_helpers.rb 136 | spec/requests/basic_spec.rb 137 | spec/requests/eof_requests_spec.rb 138 | spec/requests/unix_socket_spec.rb 139 | spec/spec_helper.rb 140 | spec/support/shared_contexts/test_server_context.rb 141 | spec/support/shared_examples/shared_example_for_clients.rb 142 | spec/support/shared_examples/shared_example_for_streaming_clients.rb 143 | spec/support/shared_examples/shared_example_for_test_servers.rb 144 | tests/authorization_header_tests.rb 145 | tests/bad_tests.rb 146 | tests/basic_tests.rb 147 | tests/complete_responses.rb 148 | tests/data/127.0.0.1.cert.crt 149 | tests/data/127.0.0.1.cert.key 150 | tests/data/excon.cert.crt 151 | tests/data/excon.cert.key 152 | tests/data/xs 153 | tests/error_tests.rb 154 | tests/header_tests.rb 155 | tests/middlewares/canned_response_tests.rb 156 | tests/middlewares/capture_cookies_tests.rb 157 | tests/middlewares/decompress_tests.rb 158 | tests/middlewares/escape_path_tests.rb 159 | tests/middlewares/idempotent_tests.rb 160 | tests/middlewares/instrumentation_tests.rb 161 | tests/middlewares/mock_tests.rb 162 | tests/middlewares/redirect_follower_tests.rb 163 | tests/pipeline_tests.rb 164 | tests/proxy_tests.rb 165 | tests/query_string_tests.rb 166 | tests/rackups/basic.rb 167 | tests/rackups/basic.ru 168 | tests/rackups/basic_auth.ru 169 | tests/rackups/deflater.ru 170 | tests/rackups/proxy.ru 171 | tests/rackups/query_string.ru 172 | tests/rackups/redirecting.ru 173 | tests/rackups/redirecting_with_cookie.ru 174 | tests/rackups/request_headers.ru 175 | tests/rackups/request_methods.ru 176 | tests/rackups/response_header.ru 177 | tests/rackups/ssl.ru 178 | tests/rackups/ssl_mismatched_cn.ru 179 | tests/rackups/ssl_verify_peer.ru 180 | tests/rackups/streaming.ru 181 | tests/rackups/thread_safety.ru 182 | tests/rackups/timeout.ru 183 | tests/rackups/webrick_patch.rb 184 | tests/request_headers_tests.rb 185 | tests/request_method_tests.rb 186 | tests/request_tests.rb 187 | tests/response_tests.rb 188 | tests/servers/bad.rb 189 | tests/servers/eof.rb 190 | tests/servers/error.rb 191 | tests/servers/good.rb 192 | tests/test_helper.rb 193 | tests/thread_safety_tests.rb 194 | tests/timeout_tests.rb 195 | tests/utils_tests.rb 196 | ] 197 | # = MANIFEST = 198 | 199 | ## Test files will be grabbed from the file list. Make sure the path glob 200 | ## matches what you actually use. 201 | s.test_files = s.files.select { |path| path =~ /^[spec|tests]\/.*_[spec|tests]\.rb/ } 202 | end 203 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | excon (0.57.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | activesupport (3.2.6) 10 | i18n (~> 0.6) 11 | multi_json (~> 1.0) 12 | backports (3.6.4) 13 | chronic (0.6.7) 14 | delorean (2.0.0) 15 | chronic 16 | diff-lcs (1.2.5) 17 | eventmachine (1.0.4) 18 | eventmachine (1.0.4-java) 19 | ffi2-generators (0.1.1) 20 | formatador (0.2.3) 21 | i18n (0.6.0) 22 | jruby-openssl (0.9.17-java) 23 | json (1.8.6) 24 | json (1.8.6-java) 25 | kgio (2.9.2) 26 | minitest (4.7.5) 27 | multi_json (1.3.6) 28 | open4 (1.3.0) 29 | puma (3.6.0) 30 | puma (3.6.0-java) 31 | rack (1.6.0) 32 | rack-protection (1.2.0) 33 | rack 34 | rack-test (0.6.3) 35 | rack (>= 1.0) 36 | raindrops (0.13.0) 37 | rake (0.9.2.2) 38 | rdoc (3.12) 39 | json (~> 1.4) 40 | rspec (3.5.0) 41 | rspec-core (~> 3.5.0) 42 | rspec-expectations (~> 3.5.0) 43 | rspec-mocks (~> 3.5.0) 44 | rspec-core (3.5.0) 45 | rspec-support (~> 3.5.0) 46 | rspec-expectations (3.5.0) 47 | diff-lcs (>= 1.2.0, < 2.0) 48 | rspec-support (~> 3.5.0) 49 | rspec-mocks (3.5.0) 50 | diff-lcs (>= 1.2.0, < 2.0) 51 | rspec-support (~> 3.5.0) 52 | rspec-support (3.5.0) 53 | rubysl (2.0.14) 54 | rubysl-abbrev (~> 2.0) 55 | rubysl-base64 (~> 2.0) 56 | rubysl-benchmark (~> 2.0) 57 | rubysl-bigdecimal (~> 2.0) 58 | rubysl-cgi (~> 2.0) 59 | rubysl-cgi-session (~> 2.0) 60 | rubysl-cmath (~> 2.0) 61 | rubysl-complex (~> 2.0) 62 | rubysl-continuation (~> 2.0) 63 | rubysl-coverage (~> 2.0) 64 | rubysl-csv (~> 2.0) 65 | rubysl-curses (~> 2.0) 66 | rubysl-date (~> 2.0) 67 | rubysl-delegate (~> 2.0) 68 | rubysl-digest (~> 2.0) 69 | rubysl-drb (~> 2.0) 70 | rubysl-e2mmap (~> 2.0) 71 | rubysl-english (~> 2.0) 72 | rubysl-enumerator (~> 2.0) 73 | rubysl-erb (~> 2.0) 74 | rubysl-etc (~> 2.0) 75 | rubysl-expect (~> 2.0) 76 | rubysl-fcntl (~> 2.0) 77 | rubysl-fiber (~> 2.0) 78 | rubysl-fileutils (~> 2.0) 79 | rubysl-find (~> 2.0) 80 | rubysl-forwardable (~> 2.0) 81 | rubysl-getoptlong (~> 2.0) 82 | rubysl-gserver (~> 2.0) 83 | rubysl-io-console (~> 2.0) 84 | rubysl-io-nonblock (~> 2.0) 85 | rubysl-io-wait (~> 2.0) 86 | rubysl-ipaddr (~> 2.0) 87 | rubysl-irb (~> 2.0) 88 | rubysl-logger (~> 2.0) 89 | rubysl-mathn (~> 2.0) 90 | rubysl-matrix (~> 2.0) 91 | rubysl-mkmf (~> 2.0) 92 | rubysl-monitor (~> 2.0) 93 | rubysl-mutex_m (~> 2.0) 94 | rubysl-net-ftp (~> 2.0) 95 | rubysl-net-http (~> 2.0) 96 | rubysl-net-imap (~> 2.0) 97 | rubysl-net-pop (~> 2.0) 98 | rubysl-net-protocol (~> 2.0) 99 | rubysl-net-smtp (~> 2.0) 100 | rubysl-net-telnet (~> 2.0) 101 | rubysl-nkf (~> 2.0) 102 | rubysl-observer (~> 2.0) 103 | rubysl-open-uri (~> 2.0) 104 | rubysl-open3 (~> 2.0) 105 | rubysl-openssl (~> 2.0) 106 | rubysl-optparse (~> 2.0) 107 | rubysl-ostruct (~> 2.0) 108 | rubysl-pathname (~> 2.0) 109 | rubysl-prettyprint (~> 2.0) 110 | rubysl-prime (~> 2.0) 111 | rubysl-profile (~> 2.0) 112 | rubysl-profiler (~> 2.0) 113 | rubysl-pstore (~> 2.0) 114 | rubysl-pty (~> 2.0) 115 | rubysl-rational (~> 2.0) 116 | rubysl-readline (~> 2.0) 117 | rubysl-resolv (~> 2.0) 118 | rubysl-rexml (~> 2.0) 119 | rubysl-rinda (~> 2.0) 120 | rubysl-rss (~> 2.0) 121 | rubysl-scanf (~> 2.0) 122 | rubysl-securerandom (~> 2.0) 123 | rubysl-set (~> 2.0) 124 | rubysl-shellwords (~> 2.0) 125 | rubysl-singleton (~> 2.0) 126 | rubysl-socket (~> 2.0) 127 | rubysl-stringio (~> 2.0) 128 | rubysl-strscan (~> 2.0) 129 | rubysl-sync (~> 2.0) 130 | rubysl-syslog (~> 2.0) 131 | rubysl-tempfile (~> 2.0) 132 | rubysl-test-unit (~> 2.0) 133 | rubysl-thread (~> 2.0) 134 | rubysl-thwait (~> 2.0) 135 | rubysl-time (~> 2.0) 136 | rubysl-timeout (~> 2.0) 137 | rubysl-tmpdir (~> 2.0) 138 | rubysl-tsort (~> 2.0) 139 | rubysl-un (~> 2.0) 140 | rubysl-uri (~> 2.0) 141 | rubysl-weakref (~> 2.0) 142 | rubysl-webrick (~> 2.0) 143 | rubysl-xmlrpc (~> 2.0) 144 | rubysl-yaml (~> 2.0) 145 | rubysl-zlib (~> 2.0) 146 | rubysl-abbrev (2.0.4) 147 | rubysl-base64 (2.0.0) 148 | rubysl-benchmark (2.0.1) 149 | rubysl-bigdecimal (2.0.2) 150 | rubysl-cgi (2.0.1) 151 | rubysl-cgi-session (2.0.1) 152 | rubysl-cmath (2.0.0) 153 | rubysl-complex (2.0.0) 154 | rubysl-continuation (2.0.0) 155 | rubysl-coverage (2.0.3) 156 | rubysl-csv (2.0.2) 157 | rubysl-english (~> 2.0) 158 | rubysl-curses (2.0.0) 159 | rubysl-date (2.0.6) 160 | rubysl-delegate (2.0.1) 161 | rubysl-digest (2.0.3) 162 | rubysl-drb (2.0.1) 163 | rubysl-e2mmap (2.0.0) 164 | rubysl-english (2.0.0) 165 | rubysl-enumerator (2.0.0) 166 | rubysl-erb (2.0.1) 167 | rubysl-etc (2.0.3) 168 | ffi2-generators (~> 0.1) 169 | rubysl-expect (2.0.0) 170 | rubysl-fcntl (2.0.4) 171 | ffi2-generators (~> 0.1) 172 | rubysl-fiber (2.0.0) 173 | rubysl-fileutils (2.0.3) 174 | rubysl-find (2.0.1) 175 | rubysl-forwardable (2.0.1) 176 | rubysl-getoptlong (2.0.0) 177 | rubysl-gserver (2.0.0) 178 | rubysl-socket (~> 2.0) 179 | rubysl-thread (~> 2.0) 180 | rubysl-io-console (2.0.0) 181 | rubysl-io-nonblock (2.0.0) 182 | rubysl-io-wait (2.0.0) 183 | rubysl-ipaddr (2.0.0) 184 | rubysl-irb (2.0.4) 185 | rubysl-e2mmap (~> 2.0) 186 | rubysl-mathn (~> 2.0) 187 | rubysl-readline (~> 2.0) 188 | rubysl-thread (~> 2.0) 189 | rubysl-logger (2.0.0) 190 | rubysl-mathn (2.0.0) 191 | rubysl-matrix (2.1.0) 192 | rubysl-e2mmap (~> 2.0) 193 | rubysl-mkmf (2.0.1) 194 | rubysl-fileutils (~> 2.0) 195 | rubysl-shellwords (~> 2.0) 196 | rubysl-monitor (2.0.0) 197 | rubysl-mutex_m (2.0.0) 198 | rubysl-net-ftp (2.0.1) 199 | rubysl-net-http (2.0.4) 200 | rubysl-cgi (~> 2.0) 201 | rubysl-erb (~> 2.0) 202 | rubysl-singleton (~> 2.0) 203 | rubysl-net-imap (2.0.1) 204 | rubysl-net-pop (2.0.1) 205 | rubysl-net-protocol (2.0.1) 206 | rubysl-net-smtp (2.0.1) 207 | rubysl-net-telnet (2.0.0) 208 | rubysl-nkf (2.0.1) 209 | rubysl-observer (2.0.0) 210 | rubysl-open-uri (2.0.0) 211 | rubysl-open3 (2.0.0) 212 | rubysl-openssl (2.0.4) 213 | rubysl-optparse (2.0.1) 214 | rubysl-shellwords (~> 2.0) 215 | rubysl-ostruct (2.0.4) 216 | rubysl-pathname (2.0.0) 217 | rubysl-prettyprint (2.0.2) 218 | rubysl-prime (2.0.0) 219 | rubysl-profile (2.0.0) 220 | rubysl-profiler (2.0.1) 221 | rubysl-pstore (2.0.0) 222 | rubysl-pty (2.0.2) 223 | rubysl-rational (2.0.1) 224 | rubysl-readline (2.0.2) 225 | rubysl-resolv (2.0.0) 226 | rubysl-rexml (2.0.2) 227 | rubysl-rinda (2.0.0) 228 | rubysl-rss (2.0.0) 229 | rubysl-scanf (2.0.0) 230 | rubysl-securerandom (2.0.0) 231 | rubysl-set (2.0.1) 232 | rubysl-shellwords (2.0.0) 233 | rubysl-singleton (2.0.0) 234 | rubysl-socket (2.0.1) 235 | rubysl-stringio (2.0.0) 236 | rubysl-strscan (2.0.0) 237 | rubysl-sync (2.0.0) 238 | rubysl-syslog (2.0.1) 239 | ffi2-generators (~> 0.1) 240 | rubysl-tempfile (2.0.1) 241 | rubysl-test-unit (2.0.1) 242 | minitest (~> 4.7) 243 | rubysl-thread (2.0.2) 244 | rubysl-thwait (2.0.0) 245 | rubysl-time (2.0.3) 246 | rubysl-timeout (2.0.0) 247 | rubysl-tmpdir (2.0.0) 248 | rubysl-tsort (2.0.1) 249 | rubysl-un (2.0.0) 250 | rubysl-fileutils (~> 2.0) 251 | rubysl-optparse (~> 2.0) 252 | rubysl-uri (2.0.0) 253 | rubysl-weakref (2.0.0) 254 | rubysl-webrick (2.0.0) 255 | rubysl-xmlrpc (2.0.0) 256 | rubysl-yaml (2.0.3) 257 | rubysl-zlib (2.0.1) 258 | shindo (0.3.4) 259 | formatador (>= 0.1.1) 260 | sinatra (1.3.2) 261 | rack (~> 1.3, >= 1.3.6) 262 | rack-protection (~> 1.2) 263 | tilt (~> 1.3, >= 1.3.3) 264 | sinatra-contrib (1.3.2) 265 | backports (>= 2.0) 266 | eventmachine 267 | rack-protection 268 | rack-test 269 | sinatra (~> 1.3.0) 270 | tilt (~> 1.3) 271 | tilt (1.3.3) 272 | unicorn (4.8.3) 273 | kgio (~> 2.6) 274 | rack 275 | raindrops (~> 0.7) 276 | 277 | PLATFORMS 278 | java 279 | ruby 280 | 281 | DEPENDENCIES 282 | activesupport 283 | delorean 284 | eventmachine (>= 1.0.4) 285 | excon! 286 | jruby-openssl (~> 0.9) 287 | json (>= 1.8.5) 288 | open4 289 | puma 290 | rack (~> 1.6) 291 | rake 292 | rdoc 293 | rspec (>= 3.5.0) 294 | rubysl (~> 2.0) 295 | shindo 296 | sinatra 297 | sinatra-contrib 298 | unicorn 299 | 300 | BUNDLED WITH 301 | 1.15.1 302 | -------------------------------------------------------------------------------- /spec/support/shared_examples/shared_example_for_clients.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | shared_examples_for 'a basic client' do |url = 'http://127.0.0.1:9292', opts = {}| 4 | # TODO: Ditch iterator and manually write a context for each set of options 5 | ([true, false] * 2).combination(2).to_a.uniq.each do |nonblock, persistent| 6 | context "when nonblock is #{nonblock} and persistent is #{persistent}" do 7 | opts = opts.merge(ssl_verify_peer: false, nonblock: nonblock, persistent: persistent) 8 | 9 | let(:conn) { Excon.new(url, opts) } 10 | 11 | context 'when :method is get and :path is /content-length/100' do 12 | describe '#request' do 13 | let(:response) do 14 | conn.request(method: :get, path: '/content-length/100') 15 | end 16 | 17 | it 'returns an Excon::Response' do 18 | expect(response).to be_instance_of Excon::Response 19 | end 20 | describe Excon::Response do 21 | describe '#status' do 22 | it 'returns 200' do 23 | expect(response.status).to eq 200 24 | end 25 | 26 | it '#status returns 200' do 27 | expect(response[:status]).to eq 200 28 | end 29 | end 30 | describe '#headers' do 31 | it '["Content-Length"] returns 100' do 32 | expect(response.headers['Content-Length']).to eq '100' 33 | end 34 | it '["Content-Type"] returns "text/html;charset=utf-8"' do 35 | expect(response.headers['Content-Type']).to eq 'text/html;charset=utf-8' 36 | end 37 | 38 | it "['Date'] returns a valid date" do 39 | if RUBY_PLATFORM == 'java' && conn.data[:scheme] == Excon::UNIX 40 | skip('until puma responds with a date header') 41 | else 42 | time = Time.parse(response.headers['Date']) 43 | expect(time.is_a?(Time)).to be true 44 | end 45 | end 46 | 47 | it "['Server'] matches /^WEBrick/" do 48 | pending('until unix_socket response has server header') if conn.data[:scheme] == Excon::UNIX 49 | expect(!!(response.headers['Server'] =~ /^WEBrick/)).to be true 50 | end 51 | 52 | it "['Custom'] returns Foo: bar" do 53 | expect(response.headers['Custom']).to eq 'Foo: bar' 54 | end 55 | end 56 | describe '#remote_ip' do 57 | it 'returns 127.0.0.1' do 58 | pending('until pigs can fly') if conn.data[:scheme] == Excon::UNIX 59 | expect(response.remote_ip).to eq '127.0.0.1' 60 | end 61 | end 62 | end 63 | 64 | context('when tcp_nodelay is true') do 65 | describe '#request' do 66 | response = nil 67 | options = opts.merge(ssl_verify_peer: false, nonblock: nonblock, tcp_nodelay: true) 68 | connection = Excon.new(url, options) 69 | 70 | it 'returns an Excon::Response' do 71 | expect do 72 | response = connection.request(method: :get, path: '/content-length/100') 73 | end.to_not raise_error 74 | end 75 | 76 | describe Excon::Response do 77 | describe '#body' do 78 | describe '.status' do 79 | it '#returns 200' do 80 | expect(response.status).to eq 200 81 | end 82 | end 83 | end 84 | end 85 | end 86 | end 87 | end 88 | 89 | context 'when utilizing deprecated block usage' do 90 | describe '#request' do 91 | data = [] 92 | it 'yields with a chunk, remaining length, and total length' do 93 | expect do 94 | conn.request(method: :get, path: '/content-length/100') do |chunk, remaining_length, total_length| 95 | data = [chunk, remaining_length, total_length] 96 | end 97 | end.to_not raise_error 98 | end 99 | it 'completes with expected data' do 100 | expect(data).to eq ['x' * 100, 0, 100] 101 | end 102 | end 103 | end 104 | 105 | context 'when utilizing response_block usage' do 106 | describe '#request' do 107 | data = [] 108 | it 'yields a chunk, remaining length, and total_length' do 109 | response_block = lambda do |chunk, remaining_length, total_length| 110 | data = [chunk, remaining_length, total_length] 111 | end 112 | expect do 113 | conn.request(method: :get, path: '/content-length/100', response_block: response_block) 114 | end.to_not raise_error 115 | end 116 | it 'completes with expected data' do 117 | expect(data).to eq ['x' * 100, 0, 100] 118 | end 119 | end 120 | end 121 | context 'when method is :post' do 122 | context 'when :path is /body-sink' do 123 | context 'when a body parameter is supplied' do 124 | response = nil 125 | it 'returns an Excon::Response' do 126 | response = conn.request(method: :post, path: '/body-sink', headers: { 'Content-Type' => 'text/plain' }, body: 'x' * 5_000_000) 127 | expect(response).to be_instance_of Excon::Response 128 | end 129 | describe Excon::Response do 130 | describe '#body' do 131 | it 'equals "5000000"' do 132 | expect(response.body).to eq '5000000' 133 | end 134 | end 135 | end 136 | end 137 | context 'when the body parameter is an empty string' do 138 | response = nil 139 | 140 | it 'returns an Excon::Response' do 141 | response = conn.request(method: :post, path: '/body-sink', headers: { 'Content-Type' => 'text/plain' }, body: '') 142 | expect(response).to be_instance_of Excon::Response 143 | end 144 | describe Excon::Response do 145 | describe '#body' do 146 | it 'equals "0"' do 147 | expect(response.body).to eq '0' 148 | end 149 | end 150 | end 151 | end 152 | end 153 | 154 | context 'when :path is /echo' do 155 | context('when a file handle is the body paramter') do 156 | describe Excon::Response do 157 | it '#body equals "x" * 100 + "\n"' do 158 | file_path = data_path('xs') 159 | response = conn.request(method: :post, path: '/echo', body: File.open(file_path)) 160 | expect(response.body).to eq 'x' * 100 + "\n" 161 | end 162 | end 163 | end 164 | 165 | context 'when a string is the body paramter' do 166 | context 'without request_block' do 167 | describe Excon::Response do 168 | it "#body equals 'x' * 100)" do 169 | response = conn.request(method: :post, path: '/echo', body: 'x' * 100) 170 | expect(response.body).to eq 'x' * 100 171 | end 172 | end 173 | end 174 | 175 | context 'when a request_block paramter is supplied' do 176 | describe Excon::Response do 177 | it "#body equals'x' * 100" do 178 | data = ['x'] * 100 179 | request_block = lambda do 180 | data.shift.to_s 181 | end 182 | response = conn.request(method: :post, path: '/echo', request_block: request_block) 183 | expect(response.body).to eq 'x' * 100 184 | end 185 | end 186 | end 187 | 188 | context('when a multi-byte string is the body paramter') do 189 | body = "\xC3\xBC" * 100 190 | headers = { 'Custom' => body.dup } 191 | if RUBY_VERSION >= '1.9' 192 | body.force_encoding('BINARY') 193 | headers['Custom'].force_encoding('UTF-8') 194 | end 195 | describe Excon::Response do 196 | it '#body properly concatenates request+headers and body' do 197 | response = conn.request(method: :post, path: '/echo', 198 | headers: headers, body: body) 199 | expect(response.body).to eq body 200 | end 201 | end 202 | end 203 | end 204 | end 205 | end 206 | end 207 | end 208 | end 209 | end 210 | --------------------------------------------------------------------------------