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.