Using Faraday

Jun 01, 2024

I've seen the faraday ruby gem used in a number of ruby projects over the years, but I hadn't used it directly myself until recently.

There are a LOT of HTTP clients in ruby land, but faraday is consistently on the top of the list as being the most popular. I decided to try it out for a project I was leading at work. I needed to build a generalized HTTP client that could be used to interact with a number of different websites that accepted and responded with different content types. I also wanted to add some defaults such as a proxy uri, basic error handling, and a timeout. I really liked faraday's approach to mixing in ad-hoc request/response middleware to handle making requests, so I decided to give it a go. I built a little utility that wraps a faraday connection and its response. Nothing too fancy, but it made working with the connection and response objects more flexible for my specific use case. The example below is simplified, but it looked something like the following:

require "faraday"
require "http-cookie"

# Wraps a Faraday::Connection
# 
# - Uses default configuration options for a proxy uri, timeout, and error handling
# - Accepts all configuration options for a faraday connection
# - Accepts a block to configure additional middleware
# - Returns a custom custom Response object
class HTTPConnection
  attr_reader :connection

  def initialize(url:, **kwargs, &block)
    @connection = Faraday.new(
      url:,
      proxy: { uri: "https://someproxy.com" },
      request: { timeout: 15 }, # seconds
      **kwargs
    ) do |builder|
      builder.response(:raise_error)
      block&.call(builder)
    end
  end

  %w[get post put delete patch].each do |request_method|
    define_method(request_method) do |path, *args|
      Response.new(connection.public_send(request_method, path, *args))
    end
  end
  
  # Wraps a Faraday::Response
  # 
  # - Adds helper to get the request_method
  # - Adds helper to get the url
  # - Adds helper to easily retrieve cookies from the response
  class Response
    extend Forwardable

    attr_reader :response, :env

    def_delegators :response, :status, :body, :headers
    def_delegators :env, :request_body, :request_headers

    def url
      env.url.to_s
    end

    def request_method
      env.method.to_s
    end

    def cookies
      Cookies.new(
        cookie_header: headers['Set-Cookie'],
        url:
      )
    end

    def initialize(response)
      @response = response
      @env = response.env
    end
  end

  class Cookies
    def initialize(cookie_header:, url:)
      @jar = HTTP::CookieJar.new
      @url = url
      @jar.parse(cookie_header.to_s, @url)
    end

    def to_a
      @jar.cookies(@url)
    end

    def to_header
      { 'Cookie' => HTTP::Cookie.cookie_value(to_a) }
    end
  end
end

conn = HTTPConnection.new(
  url: "https://example.com",
  headers: { "Content-Type" => "application/json" }
) do |builder|
  # Can add any additional middleware here, depending on the requirements of the endpoint
  builder.request(:url_encoded)
  builder.response(:json)
end

# Works just like a normal faraday request
response = conn.post('/endpoint', { 'name' => 'Jason' })