Project

hurley

0.36
No commit activity in last 3 years
No release in over 3 years
There's a lot of open issues
Hurley provides a common interface for working with different HTTP adapters.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

~> 1.0
 Project Readme

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:b@api.com/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:d@api.com/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}

TODO

  • Backport Faraday adapters as gems
    • Excon
    • Typhoeus
  • Integrate into Faraday reliant gems:
  • Tomdoc all the things
  • Fix allll the bugs
  • Release v1.0