Hurley
Hurley is a ruby gem with no runtime dependencies that provides a common interface for working with different HTTP adapters. It is an evolution of Faraday, with rethought internals.
Hurley revolves around three main classes: Client, Request, and Response. A Client sets the default properties for all HTTP requests, including the base url, headers, and options.
require "hurley"
# If you prefer Addressable::URI, require this too:
# This is required automatically if `Addressable::URI` is defined when Hurley
# is being loaded.
require "hurley/addressable"
client = Hurley::Client.new "https://api.github.com"
client.header[:accept] = "application/vnd.github+json"
client.query["a"] = "?a is set on every request too"
client.scheme # => "https"
client.host # => "api.github.com"
client.port # => 443
# See Hurley::RequestOptions in lib/hurley/options.rb
client.request_options.timeout = 3
# See Hurley::SslOptions in lib/hurley/options.rb
client.ssl_options.ca_file = "path/to/cert.crt"
# Verbs head, get, put, post, patch, delete, and options are supported.
response = client.get("users/tater") do |req|
# These properties can be changed on a per-request basis.
req.header[:accept] = "application/vnd.github.preview+json"
req.query["a"] = "override!"
req.options.timeout = 1
req.ssl_options.ca_file = "path/to/cert.crt"
req.verb # => :get
req.scheme # => "https"
req.host # => "api.github.com"
req.port # => 443
end
# You can also use Hurley class level shortcuts, which use Hurley.default_client.
response = Hurley.get("https://api.github.com/users/tater")
response.header[:content_type] # => "application/json"
response.status_code # => 200
response.body # => {"id": 1, ...}
response.request # => same as `request`
# Is this a 2xx response?
response.success?
# Is this a 3xx redirect?
response.redirection?
# Is this is a 4xx response?
response.client_error?
# Is this a 5xx response?
response.server_error?
# What kind of response is this?
response.status_type # => One of :success, :redirection, :client_error, :server_error, or :other
# Timing of the response, in ms
response.ms
# Responses automatically follow 5 redirections by default.
response.via # Array of Request objects that redirected.
response.location # => New Request built from Location header URL.
# You can tune the number of redirections, or disable them per Client or Request.
# This client follows up to 10 redirects
client.request_options.redirection_limit = 10
client.get "/foo" do |req|
# this specific request never follows any redirects.
req.options.redirection_limit = 0
end
Connections
By default, a Hurley::Client
uses a Hurley::Connection
instance to make
requests with net/http. You can swap the connection with any object that
responds to #call
with a Request, and returns a Response. This will not
interrupt other client properties or callbacks.
client = Hurley::Client.new "https://api.github.com"
client.connection = lambda do |req|
# return a Hurley::Response!
end
URLs
Hurley joins a Client endpoint with a given request URL to produce the final URL that is requested.
client = Hurley::Client.new "https://a:[email protected]/v1?a=1"
client.get "user" do |req|
req.url.user # => "a"
req.url.password # => "b"
req.url # https://api.com/v1/user?a=1
end
# Absolute paths remove any path prefix
client.get "/v2/user" do |req|
req.url.user # => "a"
req.url.password # => "b"
req.url # https://api.com/v2/user?a=1
end
client.get "user?a=2" do |req|
req.url.user # => "a"
req.url.password # => "b"
req.url # https://api.com/v1/user?a=2
end
# Basic auth can be overridden
client.get "https://c:[email protected]/v1/user" do |req|
req.url.user # => "c"
req.url.password # => "d"
req.url # https://api.com/v1/user?a=1
end
client.get "https://staging.api.com/v1/user" do |req|
req.url.user # => nil, since the host changed
req.url.password # => nil
req.url # https://staging.api.com/v1/user
end
Hurley uses Hurley::Query::Nested
for all query encoding and decoding by
default. This can be changed globally, per client, or per request. Typically
you won't create Hurley::Query
instances manually, and will use
Hurley::Query.parse
for parsing.
# Nested queries
q = Hurley::Query::Nested.new(:a => [1,2], :h => {:a => 1})
q.to_query_string # => "a[]=1&a[]=2&h[a]=1"
Hurley::Query::Nested.parse(q.to_query_string)
# => #<Hurley::Query::Nested {"a"=>["1", "2"], "h"=>{"a"=>"1"}}>
# Flat queries
q = Hurley::Query::Flat.new(:a => [1,2])
q.to_query_string # => "a=1&a=2"
Hurley::Query::Flat.parse(q.to_query_string)
# => #<Hurley::Query::Nested {"a"=>["1", "2"]}>
# Change it globally.
Hurley.default = Hurley::Query::Flat
# Change it for just this client.
client = Hurley::Client.new
client.request_options.query_class = Hurley::Query::Flat
# Change it for just this request.
client.get "/foo" do |req|
req.options.query_class = Hurley::Query::Flat
end
Headers
A Client's Header is passed down to each request. Header keys can be overridden
by the request. Headers are stored internally in canonical form, which is
capitalized with dashes: "Content-Type"
, for example.
See Hurley::Header
for all of the common header keys that have symbolized
shortcuts.
client = Hurley::Client.new "https://api.com"
client.header[:content_type] = "application/json"
# Same as:
client.header["content-type"] = "application/json"
client.header["Content-Type"] = "application/json"
client.get "/something.atom" do |req|
# Default user agent
req.header[:user_agent] # => "Hurley v#{Hurley::VERSION}"
# Change a header
req.header[:content_type] = "application/atom"
end
Posting Forms
Hurley will encode form bodies for you, while setting default Content-Type and
Content-Length values as necessary. Multipart forms are supported too, using
Hurley::UploadIO
objects.
# Works with HTTP verbs: post, put, and patch
# Send a=1 with Content-Type: application/x-www-form-urlencoded
client.post("/form", :a => 1)
# Send a=1 with Content-Type: text/plain
client.post("/form", {:a => 1}, "text/plain")
# Send file with Content-Type: multipart/form-data
client.post("/multipart", :file => Hurley::UploadIO.new("filename.txt", "text/plain"))
The default query parser (Hurley::Query::Nested
) is used by default. You can
change it globally, per client, or per request. See the "URLs" section.
Client Callbacks
Clients can define "before callbacks" that yield a Request, or "after callbacks" that yield a Response. Multiple callbacks of the same type are added in order.
client.before_call do |req|
# modify request before it's called
end
client.before_callbacks # => ["#<Proc:...>"]
client.after_call do |res|
# modify response after it's called
end
# You can set a name to identify the callback
client.before_call :upcase do |req|
req.body.upcase!
end
client.before_callbacks # => [:upcase]
# You can also pass an object that responds to #call and #name.
class Upcaser
def name
:upcaser
end
def call(req)
req.body.upcase!
end
end
client.before_call(Upcaser.new)
client.before_callbacks # => [:upcaser]
Streaming the Response Body
A Request object can take a callback that receives the response body in chunks as they are read from the socket. Hurley connections that don't support streaming will yield the entire response body once.
client.get "big-file" do |req|
req.on_body do |res, chunk|
puts "#{res.status_code}: #{chunk}"
end
# This streams the body for 200 or 201 responses only:
req.on_body(200, 201) do |res, chunk|
puts "#{res.status_code}: #{chunk}"
end
end
Testing
Hurley includes a Test connection object for testing. This lets you make requests without hitting a real endpoint.
require "hurley"
require "hurley/test"
client = Hurley::Client.new "https://api.github.com"
client.connection = Hurley::Test.new do |test|
# Verbs head, get, put, post, patch, delete, and options are supported.
test.get "/user" do |req|
# req is a Hurley::Request
# Return a Rack-compatible response.
[200, {"Content-Type" => "application/json"}, %({"id": 1})]
end
end
client.get("/user").body # => {"id": 1}