article

Sniffing API Calls in Ruby

3 min read

Sniffing API Calls in Ruby

When debugging integration issues or reverse-engineering third-party gems, you need to see what’s actually hitting the wire. Ruby offers several approaches for intercepting HTTP traffic—each with distinct trade-offs between invasiveness and visibility.

The Quick and Dirty: Environment Variables

Before writing any code, try the path of least resistance:

# Set before running your script
ENV['HTTP_PROXY'] = 'http://localhost:8080'
ENV['HTTPS_PROXY'] = 'http://localhost:8080'

Point these at a proxy like mitmproxy or Burp Suite. Many HTTP libraries respect these variables automatically—but not all. Faraday does. HTTParty does. Some custom implementations ignore them entirely.

Monkey-Patching Net::HTTP

For deeper visibility without external tools, intercept at Ruby’s standard library level:

module NetHTTPSniffer
  def request(req, body = nil, &block)
    puts "[REQUEST] #{req.method} #{@address}#{req.path}"
    req.each_header { |k, v| puts "  #{k}: #{v}" }
    puts "  Body: #{body}" if body
    
    super.tap do |response|
      puts "[RESPONSE] #{response.code}"
      puts "  Body: #{response.body[0..500]}..."
    end
  end
end

Net::HTTP.prepend(NetHTTPSniffer)

This catches everything built on Net::HTTP—which includes most gems. The trade-off: you’re modifying core library behaviour. Keep this out of production.

Webmock for Selective Interception

When you need surgical precision rather than blanket capture:

require 'webmock'

WebMock.after_request do |request, response|
  puts "#{request.method.upcase} #{request.uri}"
  puts "Request headers: #{request.headers}"
  puts "Response: #{response.status} - #{response.body[0..200]}"
end

WebMock.allow_net_connect!

Webmock hooks into multiple HTTP libraries simultaneously. The allow_net_connect! call lets requests through while still logging them—useful when you want observation without blocking.

Faraday Middleware Approach

If you control the HTTP client, middleware provides the cleanest solution:

require 'faraday'

conn = Faraday.new(url: 'https://api.example.com') do |f|
  f.response :logger, Logger.new($stdout), bodies: true
  f.adapter Faraday.default_adapter
end

response = conn.get('/users')

For custom logging logic:

class SnifferMiddleware < Faraday::Middleware
  def call(env)
    puts ">>> #{env.method.upcase} #{env.url}"
    @app.call(env).on_complete do |response_env|
      puts "<<< #{response_env.status}"
    end
  end
end

Faraday.new do |f|
  f.use SnifferMiddleware
  f.adapter :net_http
end

When to Use What

Environment variables: First attempt. Zero code changes, but library support varies.

Net::HTTP patching: Maximum coverage for debugging sessions. Never in production.

Webmock: Test environments or when you need conditional interception.

Faraday middleware: Production-safe logging when you own the client instantiation.

Handling HTTPS

SSL interception requires certificate trust. With mitmproxy:

# Install mitmproxy CA certificate
mitmproxy --mode regular
# Then in Ruby
ENV['SSL_CERT_FILE'] = '/path/to/mitmproxy-ca-cert.pem'

Or disable verification temporarily (development only):

OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE

This is dangerous. Use it for debugging, then delete the line.

Final Thoughts

Start with the least invasive method that gives you the visibility you need. Proxy-based sniffing keeps your codebase clean. Monkey-patching gives maximum insight at the cost of fragility. Middleware strikes the best balance when you’re building something meant to last.