mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-10-06 00:02:08 -04:00
added faraday adapter and tests
This commit is contained in:
parent
4fee26b313
commit
f3c3be0c8a
201
lib/httpx/adapters/faraday.rb
Normal file
201
lib/httpx/adapters/faraday.rb
Normal file
@ -0,0 +1,201 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "faraday"
|
||||
|
||||
module Faraday
|
||||
class Adapter
|
||||
class HTTPX < Faraday::Adapter
|
||||
module RequestMixin
|
||||
private
|
||||
|
||||
def build_request(env)
|
||||
meth = env[:method]
|
||||
|
||||
request_options = {
|
||||
headers: env.request_headers,
|
||||
body: env.body,
|
||||
}
|
||||
[meth, env.url, request_options]
|
||||
end
|
||||
end
|
||||
|
||||
include RequestMixin
|
||||
|
||||
class Client < ::HTTPX::Client
|
||||
plugin(:compression)
|
||||
|
||||
module ReasonPlugin
|
||||
if RUBY_VERSION < "2.5"
|
||||
def self.load_dependencies(*)
|
||||
require "webrick"
|
||||
end
|
||||
else
|
||||
def self.load_dependencies(*)
|
||||
require "net/http/status"
|
||||
end
|
||||
end
|
||||
module ResponseMethods
|
||||
if RUBY_VERSION < "2.5"
|
||||
def reason
|
||||
WEBrick::HTTPStatus::StatusMessage[@status]
|
||||
end
|
||||
else
|
||||
def reason
|
||||
Net::HTTP::STATUS_CODES[@status]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
plugin(ReasonPlugin)
|
||||
end
|
||||
|
||||
class ParallelManager
|
||||
class ResponseHandler
|
||||
attr_reader :env
|
||||
|
||||
def initialize(env)
|
||||
@env = env
|
||||
end
|
||||
|
||||
def on_response(&blk)
|
||||
if block_given?
|
||||
@on_response = lambda do |response|
|
||||
blk.call(response)
|
||||
end
|
||||
self
|
||||
else
|
||||
@on_response
|
||||
end
|
||||
end
|
||||
|
||||
def on_complete(&blk)
|
||||
if block_given?
|
||||
@on_complete = blk
|
||||
self
|
||||
else
|
||||
@on_complete
|
||||
end
|
||||
end
|
||||
|
||||
def respond_to_missing?(meth)
|
||||
@env.respond_to?(meth)
|
||||
end
|
||||
|
||||
def method_missing(meth, *args, &blk)
|
||||
if @env && @env.respond_to?(meth)
|
||||
@env.__send__(meth, *args, &blk)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include RequestMixin
|
||||
|
||||
def initialize
|
||||
@client = Client.new
|
||||
@handlers = []
|
||||
end
|
||||
|
||||
def enqueue(request)
|
||||
handler = ResponseHandler.new(request)
|
||||
@handlers << handler
|
||||
handler
|
||||
end
|
||||
|
||||
def run
|
||||
requests = @handlers.map { |handler| build_request(handler.env) }
|
||||
env = @handlers.last.env
|
||||
|
||||
timeout_options = {
|
||||
connect_timeout: env.request.open_timeout,
|
||||
operation_timeout: env.request.timeout,
|
||||
}.reject { |_, v| v.nil? }
|
||||
|
||||
options = {
|
||||
ssl: env.ssl,
|
||||
timeout: timeout_options,
|
||||
}
|
||||
|
||||
proxy_options = { uri: env.request.proxy }
|
||||
|
||||
client = @client.with(options)
|
||||
client = client.plugin(:proxy).with_proxy(proxy_options) if env.request.proxy
|
||||
|
||||
responses = client.request(requests)
|
||||
responses.each_with_index do |response, index|
|
||||
handler = @handlers[index]
|
||||
handler.on_response.call(response)
|
||||
handler.on_complete.call(handler.env)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.supports_parallel = true
|
||||
|
||||
class << self
|
||||
def setup_parallel_manager
|
||||
ParallelManager.new
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(app)
|
||||
super(app)
|
||||
@client = Client.new
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if parallel?(env)
|
||||
handler = env[:parallel_manager].enqueue(env)
|
||||
handler.on_response do |response|
|
||||
save_response(env, response.status, response.body, response.headers, response.reason) do |response_headers|
|
||||
response_headers.merge!(response.headers)
|
||||
end
|
||||
end
|
||||
return handler
|
||||
end
|
||||
|
||||
request_options = build_request(env)
|
||||
|
||||
timeout_options = {
|
||||
connect_timeout: env.request.open_timeout,
|
||||
operation_timeout: env.request.timeout,
|
||||
}.reject { |_, v| v.nil? }
|
||||
|
||||
options = {
|
||||
ssl: env.ssl,
|
||||
timeout: timeout_options,
|
||||
}
|
||||
|
||||
proxy_options = { uri: env.request.proxy }
|
||||
|
||||
client = @client.with(options)
|
||||
client = client.plugin(:proxy).with_proxy(proxy_options) if env.request.proxy
|
||||
response = client.__send__(*request_options)
|
||||
response.raise_for_status unless response.is_a?(::HTTPX::Response)
|
||||
save_response(env, response.status, response.body, response.headers, response.reason) do |response_headers|
|
||||
response_headers.merge!(response.headers)
|
||||
end
|
||||
@app.call(env)
|
||||
rescue OpenSSL::SSL::SSLError => err
|
||||
raise Error::SSLError, err
|
||||
rescue Errno::ECONNABORTED,
|
||||
Errno::ECONNREFUSED,
|
||||
Errno::ECONNRESET,
|
||||
Errno::EHOSTUNREACH,
|
||||
Errno::EINVAL,
|
||||
Errno::ENETUNREACH,
|
||||
Errno::EPIPE => err
|
||||
raise Error::ConnectionFailed, err
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parallel?(env)
|
||||
env[:parallel_manager]
|
||||
end
|
||||
end
|
||||
|
||||
register_middleware httpx: HTTPX
|
||||
end
|
||||
end
|
192
test/adapters/faraday_test.rb
Normal file
192
test/adapters/faraday_test.rb
Normal file
@ -0,0 +1,192 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../test_helper"
|
||||
require "faraday"
|
||||
require "forwardable"
|
||||
require "httpx/adapters/faraday"
|
||||
require_relative "../support/http_helpers"
|
||||
|
||||
class FaradayTest < Minitest::Test
|
||||
extend Forwardable
|
||||
include HTTPHelpers
|
||||
include ProxyHelper
|
||||
|
||||
def_delegators :create_connection, :get, :head, :put, :post, :patch, :delete, :run_request
|
||||
|
||||
def test_in_parallel
|
||||
resp1, resp2 = nil, nil
|
||||
|
||||
connection = create_connection
|
||||
connection.in_parallel do
|
||||
resp1 = connection.get(build_path("/get?a=1"))
|
||||
resp2 = connection.get(build_path("/get?b=2"))
|
||||
assert connection.in_parallel?
|
||||
assert_nil resp1.reason_phrase
|
||||
assert_nil resp2.reason_phrase
|
||||
end
|
||||
assert !connection.in_parallel?
|
||||
assert_equal "OK", resp1.reason_phrase
|
||||
assert_equal "OK", resp2.reason_phrase
|
||||
end
|
||||
|
||||
def test_get_handles_compression
|
||||
res = get(build_path("/gzip"))
|
||||
assert JSON.parse(res.body.to_s)["gzipped"]
|
||||
end
|
||||
|
||||
def test_get_ssl_fails_with_bad_cert
|
||||
ca_file = "tmp/faraday-different-ca-cert.crt"
|
||||
conn = create_connection(ssl: { ca_file: ca_file })
|
||||
err = assert_raises Faraday::SSLError do
|
||||
conn.get("/ssl")
|
||||
end
|
||||
assert_includes err.message, "certificate"
|
||||
end
|
||||
|
||||
def test_get_send_url_encoded_params
|
||||
assert_equal({ "name" => "zack" }, JSON.parse(get(build_path("/get"), name: "zack").body.to_s)["args"])
|
||||
end
|
||||
|
||||
def test_get_retrieves_the_response_headers
|
||||
response = get(build_path("/get"))
|
||||
assert_match(%r{application/json}, response.headers["Content-Type"], "original case fail")
|
||||
assert_match(%r{application/json}, response.headers["content-type"], "lowercase fail")
|
||||
end
|
||||
|
||||
def test_get_sends_user_agent
|
||||
response = get(build_path("/user-agent"), { name: "user-agent" }, user_agent: "Agent Faraday")
|
||||
assert_equal "Agent Faraday", JSON.parse(response.body.to_s)["user-agent"]
|
||||
end
|
||||
|
||||
def test_get_reason_phrase
|
||||
response = get(build_path("/get"))
|
||||
assert_equal "OK", response.reason_phrase
|
||||
end
|
||||
|
||||
def test_post_send_url_encoded_params
|
||||
json = JSON.parse post(build_path("/post"), name: "zack").body
|
||||
assert_equal({ "name" => "zack" }, json["form"])
|
||||
end
|
||||
|
||||
def test_post_send_url_encoded_nested_params
|
||||
resp = post(build_path("/post"), "name" => { "first" => "zack" })
|
||||
json = JSON.parse resp.body.to_s
|
||||
assert_equal({ "name[first]" => "zack" }, json["form"])
|
||||
end
|
||||
|
||||
def test_post_retrieves_the_response_headers
|
||||
assert_match(%r{application/json}, post(build_path("/post")).headers["content-type"])
|
||||
end
|
||||
|
||||
def test_post_sends_files
|
||||
resp = post(build_path("/post")) do |req|
|
||||
req.body = { "uploaded_file" => Faraday::UploadIO.new(__FILE__, "text/x-ruby") }
|
||||
end
|
||||
json = JSON.parse resp.body.to_s
|
||||
assert json.key?("files")
|
||||
assert json["files"].key?("uploaded_file")
|
||||
assert_equal(json["files"]["uploaded_file"].bytesize, File.size(__FILE__))
|
||||
end
|
||||
|
||||
def test_put_send_url_encoded_params
|
||||
json = JSON.parse put(build_path("/put"), name: "zack").body.to_s
|
||||
assert_equal({ "name" => "zack" }, json["form"])
|
||||
end
|
||||
|
||||
def test_put_send_url_encoded_nested_params
|
||||
resp = put(build_path("/put"), "name" => { "first" => "zack" })
|
||||
json = JSON.parse resp.body.to_s
|
||||
assert_equal({ "name[first]" => "zack" }, json["form"])
|
||||
end
|
||||
|
||||
def test_put_retrieves_the_response_headers
|
||||
assert_match(%r{application/json}, put(build_path("/put")).headers["content-type"])
|
||||
end
|
||||
|
||||
def test_patch_send_url_encoded_params
|
||||
json = JSON.parse patch(build_path("/patch"), name: "zack").body.to_s
|
||||
assert_equal({ "name" => "zack" }, json["form"])
|
||||
end
|
||||
|
||||
def test_head_retrieves_no_response_body
|
||||
assert_equal "", head(build_path("/get")).body.to_s
|
||||
end
|
||||
|
||||
def test_head_retrieves_the_response_headers
|
||||
assert_match(%r{application/json}, head(build_path("/get")).headers["content-type"])
|
||||
end
|
||||
|
||||
def test_delete_retrieves_the_response_headers
|
||||
assert_match(%r{application/json}, delete(build_path("/delete")).headers["content-type"])
|
||||
end
|
||||
|
||||
# def test_timeout
|
||||
# conn = create_connection(request: { timeout: 1, open_timeout: 1 })
|
||||
# assert_raises Faraday::Error::TimeoutError do
|
||||
# conn.get(build_path("/delay/5"))
|
||||
# end
|
||||
# end
|
||||
|
||||
def test_connection_error
|
||||
assert_raises Faraday::Error::ConnectionFailed do
|
||||
get "http://localhost:4"
|
||||
end
|
||||
end
|
||||
|
||||
# def test_proxy
|
||||
# proxy_uri = http_proxy.first
|
||||
# conn = create_connection(proxy: proxy_uri)
|
||||
|
||||
# res = conn.get(build_path("/get"))
|
||||
# assert res.status == 200
|
||||
|
||||
# unless self.class.ssl_mode?
|
||||
# # proxy can't append "Via" header for HTTPS responses
|
||||
# assert_match(/:#{proxy_uri.port}$/, res["via"])
|
||||
# end
|
||||
# end
|
||||
|
||||
# def test_proxy_auth_fail
|
||||
# proxy_uri = URI(ENV["LIVE_PROXY"])
|
||||
# proxy_uri.password = "WRONG"
|
||||
# conn = create_connection(proxy: proxy_uri)
|
||||
|
||||
# err = assert_raises Faraday::Error::ConnectionFailed do
|
||||
# conn.get "/echo"
|
||||
# end
|
||||
# end
|
||||
|
||||
private
|
||||
|
||||
def origin(orig = httpbin)
|
||||
"https://#{orig}"
|
||||
end
|
||||
|
||||
def build_path(ph)
|
||||
URI(build_uri(ph)).path
|
||||
end
|
||||
|
||||
# extra options to pass when building the adapter
|
||||
def adapter_options
|
||||
[]
|
||||
end
|
||||
|
||||
def create_connection(options = {}, &optional_connection_config_blk)
|
||||
builder_block = proc do |b|
|
||||
b.request :multipart
|
||||
b.request :url_encoded
|
||||
b.adapter :httpx, *adapter_options, &optional_connection_config_blk
|
||||
end
|
||||
|
||||
options[:ssl] ||= {}
|
||||
options[:ssl][:ca_file] ||= ENV["SSL_FILE"]
|
||||
|
||||
server = URI("https://#{httpbin}")
|
||||
|
||||
Faraday::Connection.new(server.to_s, options, &builder_block).tap do |conn|
|
||||
conn.headers["X-Faraday-Adapter"] = "httpx"
|
||||
adapter_handler = conn.builder.handlers.last
|
||||
conn.builder.insert_before adapter_handler, Faraday::Response::RaiseError
|
||||
end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user