Se você está construindo pipelines de dados, scrapers ou sistemas de monitoramento em Ruby, eventualmente precisará rotear requisições através de proxies. Seja para evitar rate limits, contornar bloqueios geográficos ou distribuir requisições entre múltiplos IPs, dominar o uso de proxies HTTP é essencial.
Este guia cobre três abordagens: a biblioteca padrão Net::HTTP, o poderoso Typhoeus com suporte a requisições paralelas, e o ProxyHat SDK para rotação automática e geo-targeting. Todos os exemplos são prontos para produção, com tratamento de erros e configurações de TLS.
Net::HTTP: Proxy Básico com Autenticação
O Net::HTTP é parte da biblioteca padrão do Ruby, tornando-o ideal para projetos que não podem adicionar dependências externas. A configuração de proxy é direta, mas requer atenção à autenticação e tratamento de erros.
Configuração Básica de Proxy
require 'net/http'
require 'uri'
# Configuração do proxy ProxyHat
PROXY_HOST = 'gate.proxyhat.com'
PROXY_PORT = 8080
PROXY_USER = 'seu_usuario'
PROXY_PASS = 'sua_senha'
def fetch_with_proxy(url, proxy_user: PROXY_USER, proxy_pass: PROXY_PASS)
uri = URI.parse(url)
# Cria conexão HTTP através do proxy
http = Net::HTTP.new(uri.host, uri.port, PROXY_HOST, PROXY_PORT, proxy_user, proxy_pass)
# Configuração TLS/SSL
http.use_ssl = uri.scheme == 'https'
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.open_timeout = 15
http.read_timeout = 30
# Constrói a requisição
request = Net::HTTP::Get.new(uri.request_uri)
request['User-Agent'] = 'ProxyHat-Ruby/1.0'
request['Accept'] = 'text/html,application/json'
# Executa com tratamento de erros
response = http.request(request)
case response
when Net::HTTPSuccess
{ status: response.code.to_i, body: response.body, headers: response.each_header.to_h }
when Net::HTTPRedirection
fetch_with_proxy(response['location'], proxy_user: proxy_user, proxy_pass: proxy_pass)
else
raise "HTTP Error #{response.code}: #{response.message}"
end
rescue Net::OpenTimeout => e
raise "Timeout de conexão: #{e.message}"
rescue Net::ReadTimeout => e
raise "Timeout de leitura: #{e.message}"
rescue SocketError => e
raise "Erro de DNS/rede: #{e.message}"
rescue OpenSSL::SSL::SSLError => e
raise "Erro SSL/TLS: #{e.message}"
ensure
http&.finish if http&.started?
end
# Exemplo de uso
result = fetch_with_proxy('https://httpbin.org/ip')
puts "Status: #{result[:status]}"
puts "IP detectado: #{JSON.parse(result[:body])['origin']}"
Proxy com Sessão Persistente (Sticky Session)
Para manter o mesmo IP durante múltiplas requisições, use o flag de sessão no username do ProxyHat:
require 'net/http'
require 'uri'
class StickyProxySession
def initialize(username, password, session_id)
@proxy_host = 'gate.proxyhat.com'
@proxy_port = 8080
# Formato: usuario-session-SESSION_ID
@proxy_user = "#{username}-session-#{session_id}"
@proxy_pass = password
@connections = {}
end
def get(url)
uri = URI.parse(url)
key = "#{uri.host}:#{uri.port}"
# Reutiliza conexão existente ou cria nova
http = @connections[key] ||= begin
Net::HTTP.new(uri.host, uri.port, @proxy_host, @proxy_port, @proxy_user, @proxy_pass).tap do |h|
h.use_ssl = uri.scheme == 'https'
h.verify_mode = OpenSSL::SSL::VERIFY_PEER
h.keep_alive_timeout = 60
h.start
end
end
request = Net::HTTP::Get.new(uri.request_uri)
http.request(request)
rescue Errno::ECONNRESET, IOError
# Reconecta em caso de falha
@connections.delete(key)
retry
end
def close_all
@connections.each_value(&:finish)
@connections.clear
end
end
# Uso: mesma sessão = mesmo IP
session = StickyProxySession.new('user', 'pass', 'order-12345')
begin
5.times do |i|
response = session.get('https://httpbin.org/ip')
puts "Requisição #{i + 1}: #{response.body}"
end
ensure
session.close_all
end
Typhoeus: Requisições Paralelas com Hydra
O Typhoeus é um wrapper Ruby para libcurl, oferecendo performance superior e suporte nativo a requisições paralelas através do Hydra. É ideal para scraping em escala.
Configuração do Typhoeus com Proxy
require 'typhoeus'
# Configuração global do proxy (opcional)
Typhoeus.configure do |config|
config.user_agent = 'ProxyHat-Typhoeus/2.0'
config.verbose = false
end
def create_request(url, proxy_user: 'user', proxy_pass: 'pass', country: nil)
# Geo-targeting via username
username = country ? "#{proxy_user}-country-#{country}" : proxy_user
proxy_url = "http://#{username}:#{proxy_pass}@gate.proxyhat.com:8080"
Typhoeus::Request.new(
url,
method: :get,
proxy: proxy_url,
headers: {
'Accept' => 'text/html,application/xhtml+xml,application/json',
'Accept-Language' => 'pt-BR,pt;q=0.9,en;q=0.8'
},
timeout: 30,
connecttimeout: 15,
followlocation: true,
ssl_verifypeer: true,
ssl_verifyhost: 2
)
end
# Requisição única com proxy
request = create_request('https://httpbin.org/ip', proxy_user: 'seu_user', proxy_pass: 'sua_pass')
response = request.run
if response.success?
puts "Status: #{response.code}"
puts "Body: #{response.body}"
elsif response.timed_out?
puts "Timeout!"
elsif response.code == 0
puts "Erro de conexão: #{response.return_message}"
else
puts "HTTP #{response.code}: #{response.status_message}"
end
Hydra: Requisições Paralelas em Lote
require 'typhoeus'
class ParallelScraper
def initialize(username, password, concurrency: 50)
@username = username
@password = password
@concurrency = concurrency
@results = Queue.new
end
def fetch_all(urls, country: nil)
hydra = Typhoeus::Hydra.new(max_concurrency: @concurrency)
urls.each_with_index do |url, index|
request = build_request(url, country: country)
request.on_complete do |response|
@results << {
index: index,
url: url,
status: response.code,
body: response.body,
total_time: response.total_time,
success: response.success?
}
end
hydra.queue(request)
end
# Executa todas as requisições
hydra.run
collect_results(urls.size)
end
private
def build_request(url, country: nil)
username = country ? "#{@username}-country-#{country}" : @username
proxy_url = "http://#{username}:#{@password}@gate.proxyhat.com:8080"
Typhoeus::Request.new(
url,
method: :get,
proxy: proxy_url,
timeout: 30,
connecttimeout: 15,
followlocation: true,
ssl_verifypeer: true
)
end
def collect_results(expected_count)
results = []
expected_count.times { results << @results.pop }
results.sort_by { |r| r[:index] }
end
end
# Exemplo: scraping de 100 URLs em paralelo
scraper = ParallelScraper.new('seu_user', 'sua_pass', concurrency: 25)
urls = 100.times.map { |i| "https://httpbin.org/delay/#{rand(1..3)}" }
results = scraper.fetch_all(urls, country: 'US')
success_count = results.count { |r| r[:success] }
avg_time = results.sum { |r| r[:total_time] } / results.size
puts "Sucesso: #{success_count}/#{urls.size}"
puts "Tempo médio: #{'%.2f' % avg_time}s"
ProxyHat SDK: Rotação Automática e Geo-Targeting
O ProxyHat SDK encapsula a lógica de rotação de IPs, geo-targeting e tratamento de erros, permitindo focar na lógica de negócio.
require 'net/http'
require 'json'
module ProxyHat
class Client
GATEWAY_HOST = 'gate.proxyhat.com'
GATEWAY_PORT = 8080
attr_reader :username, :password
def initialize(username:, password:, session: nil, country: nil, city: nil)
@username = build_username(username, session: session, country: country, city: city)
@password = password
@retry_count = 0
@max_retries = 3
end
def get(url, headers: {})
request_with_retry(:get, url, headers: headers)
end
def post(url, body:, headers: {})
request_with_retry(:post, url, body: body, headers: headers)
end
def rotate!
@session_id = SecureRandom.hex(8)
self
end
private
def build_username(base, session: nil, country: nil, city: nil)
parts = [base]
parts << "session-#{session || SecureRandom.hex(8)}"
parts << "country-#{country}" if country
parts << "city-#{city}" if city
parts.join('-')
end
def request_with_retry(method, url, body: nil, headers: {})
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port, GATEWAY_HOST, GATEWAY_PORT, @username, @password)
http.use_ssl = uri.scheme == 'https'
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.open_timeout = 15
http.read_timeout = 30
request = build_request(method, uri, body, headers)
response = http.request(request)
handle_response(response, method, url, body, headers)
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET => e
@retry_count += 1
raise e if @retry_count > @max_retries
sleep(2 ** @retry_count)
rotate!
retry
ensure
http&.finish if http&.started?
end
def build_request(method, uri, body, headers)
klass = method == :get ? Net::HTTP::Get : Net::HTTP::Post
request = klass.new(uri.request_uri)
headers.each { |k, v| request[k] = v }
request['User-Agent'] ||= 'ProxyHat-SDK/1.0'
request.body = body.to_json if body && method == :post
request
end
def handle_response(response, *retry_args)
case response
when Net::HTTPSuccess
{ status: response.code.to_i, body: response.body, headers: response.each_header.to_h }
when Net::HTTPTooManyRequests
@retry_count += 1
raise 'Max retries exceeded' if @retry_count > @max_retries
sleep(response['Retry-After']&.to_i || 60)
request_with_retry(*retry_args)
when Net::HTTPRedirection
request_with_retry(:get, response['location'])
else
raise "HTTP #{response.code}: #{response.message}"
end
end
end
# Pool para múltiplas sessões
class ProxyPool
def initialize(username:, password:, pool_size: 10, country: nil)
@clients = pool_size.times.map do |i|
Client.new(
username: username,
password: password,
session: "pool-#{i}-#{SecureRandom.hex(4)}",
country: country
)
end
@mutex = Mutex.new
@index = 0
end
def with_client
client = @mutex.synchronize do
@index = (@index + 1) % @clients.size
@clients[@index]
end
yield client
end
end
end
# Uso do SDK
client = ProxyHat::Client.new(
username: 'seu_usuario',
password: 'sua_senha',
country: 'BR',
city: 'sao-paulo'
)
result = client.get('https://httpbin.org/ip')
puts "IP brasileiro: #{JSON.parse(result[:body])['origin']}"
# Rotação manual
client.rotate!
result = client.get('https://httpbin.org/ip')
puts "Novo IP: #{JSON.parse(result[:body])['origin']}"
Scraping em Escala: 1000 URLs com Proxies Rotativos
Para scraping de larga escala, combinamos Typhoeus Hydra com o pool de proxies do ProxyHat, implementando circuit breaker e logging estruturado.
require 'typhoeus'
require 'json'
require 'logger'
module ProxyHat
class ScalableScraper
def initialize(username:, password:, concurrency: 100, country: 'US')
@username = username
@password = password
@concurrency = concurrency
@country = country
@logger = Logger.new($stdout).tap { |l| l.level = Logger::INFO }
@stats = { success: 0, failed: 0, retries: 0 }
@stats_mutex = Mutex.new
end
def scrape(urls)
@start_time = Time.now
results = Concurrent::Array.new
hydra = Typhoeus::Hydra.new(max_concurrency: @concurrency)
urls.each_with_index do |url, idx|
request = build_rotating_request(url, idx)
request.on_complete do |response|
result = process_response(response, url, idx)
results << result
update_stats(result)
end
hydra.queue(request)
end
hydra.run
log_summary(results.size)
results
end
private
def build_rotating_request(url, index)
# Cada requisição usa um IP diferente (rotação por requisição)
session_id = "req-#{index}-#{SecureRandom.hex(4)}"
username = "#{@username}-country-#{@country}-session-#{session_id}"
proxy_url = "http://#{username}:#{@password}@gate.proxyhat.com:8080"
Typhoeus::Request.new(
url,
method: :get,
proxy: proxy_url,
timeout: 45,
connecttimeout: 20,
followlocation: true,
ssl_verifypeer: true,
headers: {
'User-Agent' => random_user_agent,
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.9',
'Accept-Encoding' => 'gzip, deflate'
}
)
end
def process_response(response, url, index)
{
index: index,
url: url,
status: response.code,
success: response.success?,
body: response.body,
time: response.total_time,
ip: extract_ip_from_proxy(response),
error: response.success? ? nil : response.return_message
}
end
def update_stats(result)
@stats_mutex.synchronize do
if result[:success]
@stats[:success] += 1
else
@stats[:failed] += 1
end
end
end
def extract_ip_from_proxy(response)
# Alguns proxies retornam o IP no header X-Proxy-IP
response.headers['X-Proxy-IP'] || 'unknown'
rescue
'unknown'
end
def random_user_agent
agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
]
agents.sample
end
def log_summary(total)
elapsed = Time.now - @start_time
rate = total / elapsed
@logger.info("=" * 50)
@logger.info("Scraping completo!")
@logger.info("Total: #{total} URLs")
@logger.info("Sucesso: #{@stats[:success]} | Falhas: #{@stats[:failed]}")
@logger.info("Tempo: #{'%.2f' % elapsed}s")
@logger.info("Taxa: #{'%.2f' % rate} req/s")
@logger.info("=" * 50)
end
end
end
# Execução do scraper em escala
scraper = ProxyHat::ScalableScraper.new(
username: 'seu_usuario',
password: 'sua_senha',
concurrency: 100,
country: 'US'
)
# Gera 1000 URLs de exemplo
urls = 1000.times.map { |i| "https://httpbin.org/delay/#{rand(1..2)}?id=#{i}" }
results = scraper.scrape(urls)
# Exporta resultados bem-sucedidos
successful = results.select { |r| r[:success] }
puts "\nExportados #{successful.size} resultados válidos"
Configuração TLS/SSL: Certificados Self-Signed e SNI
Ao usar proxies, você pode encontrar servidores com certificados self-signed ou necessitar de configuração SNI (Server Name Indication) específica.
require 'net/http'
require 'openssl'
module ProxyHat
class TLSClient
def initialize(username:, password:, verify_mode: :peer)
@username = username
@password = password
@verify_mode = verify_mode
@ca_bundle = ENV.fetch('SSL_CERT_FILE', nil)
end
def get(url, sni_hostname: nil, allow_self_signed: false)
uri = URI.parse(url)
proxy_url = "http://#{@username}:#{@password}@gate.proxyhat.com:8080"
http = Net::HTTP.new(uri.host, uri.port, 'gate.proxyhat.com', 8080, @username, @password)
http.use_ssl = uri.scheme == 'https'
# Configuração SSL/TLS
configure_ssl(http, allow_self_signed)
# SNI customizado (útil para hosts virtuais)
if sni_hostname
http.enable_post_connection_check = false
end
request = Net::HTTP::Get.new(uri.request_uri)
request['Host'] = sni_hostname if sni_hostname
response = http.request(request)
{ status: response.code.to_i, body: response.body }
rescue OpenSSL::SSL::SSLError => e
handle_ssl_error(e, url)
ensure
http&.finish if http&.started?
end
private
def configure_ssl(http, allow_self_signed)
case @verify_mode
when :none
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
when :peer
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.ca_file = @ca_bundle if @ca_bundle
when :fail_if_no_peer_cert
http.verify_mode = OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
end
# Permite certificados self-signed (APENAS em desenvolvimento!)
if allow_self_signed
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
warn '[AVISO] Verificação SSL desabilitada - não use em produção!'
end
# Configura versões TLS
http.min_version = OpenSSL::SSL::TLS1_2_VERSION
http.max_version = OpenSSL::SSL::TLS1_3_VERSION
# Cipher suites seguros
http.ciphers = 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256'
end
def handle_ssl_error(error, url)
case error.message
when /certificate verify failed/
raise "Certificado inválido para #{url}. Verifique a CA ou use allow_self_signed: true (dev only)"
when /hostname does not match/
raise "Hostname mismatch. Use sni_hostname para especificar o hostname correto"
when /handshake failure/
raise "Falha no handshake TLS. Verifique versões/ciphers suportados"
else
raise "Erro SSL: #{error.message}"
end
end
end
end
# Exemplo com configurações TLS
client = ProxyHat::TLSClient.new(
username: 'seu_usuario',
password: 'sua_senha',
verify_mode: :peer
)
# Requisição normal
result = client.get('https://httpbin.org/get')
puts "Status: #{result[:status]}"
# Com SNI customizado (para CDNs/load balancers)
result = client.get('https://example.com/api', sni_hostname: 'api.example.com')
Integração com Rails: Faraday Middleware e ActiveJob
Em aplicações Rails, a integração de proxies é mais elegante usando Faraday como cliente HTTP e ActiveJob para processamento assíncrono.
Middleware Faraday para Proxy
# config/initializers/proxy_hat.rb
require 'faraday'
require 'faraday/retry'
module ProxyHat
class FaradayMiddleware < Faraday::Middleware
def initialize(app, username:, password:, country: nil, rotate: true)
super(app)
@username = username
@password = password
@country = country
@rotate = rotate
end
def call(env)
# Adiciona configuração de proxy à requisição
env[:proxy] = build_proxy_config
# Retry automático em caso de falha de proxy
retries = 0
max_retries = 3
begin
@app.call(env)
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
retries += 1
raise e if retries > max_retries
Rails.logger.warn "Proxy falhou, tentando novo IP (tentativa #{retries})"
env[:proxy] = build_proxy_config(force_new_ip: true)
retry
end
end
private
def build_proxy_config(force_new_ip: false)
session = @rotate ? SecureRandom.hex(8) : 'fixed-session'
username = "#{@username}-country-#{@country}-session-#{session}"
{
uri: 'http://gate.proxyhat.com:8080',
user: username,
password: @password
}
end
end
end
# Configuração do Faraday
Rails.application.config.faraday_proxy_client = Faraday.new do |f|
f.use ProxyHat::FaradayMiddleware,
username: ENV['PROXYHAT_USERNAME'],
password: ENV['PROXYHAT_PASSWORD'],
country: 'US',
rotate: true
f.request :retry, {
max: 3,
interval: 1.0,
backoff_factor: 2,
retry_statuses: [429, 500, 502, 503, 504]
}
f.response :json
f.adapter :net_http_persistent
end
# app/services/scraper_service.rb
class ScraperService
def initialize
@client = Rails.application.config.faraday_proxy_client
end
def fetch(url)
response = @client.get(url)
response.body
rescue Faraday::Error => e
Rails.logger.error "Erro ao buscar #{url}: #{e.message}"
nil
end
end
ActiveJob para Scraping em Background
# app/jobs/scraping_job.rb
class ScrapingJob < ApplicationJob
queue_as :scraping
# Evita duplicatas
sidekiq_options lock: :until_executed, lock_ttl: 3600 if defined?(Sidekiq)
def perform(urls, options = {})
@country = options.fetch(:country, 'US')
@concurrency = options.fetch(:concurrency, 50)
@batch_size = options.fetch(:batch_size, 100)
results = []
urls.each_slice(@batch_size) do |batch|
batch_results = process_batch(batch)
results.concat(batch_results)
# Atualiza progresso
update_progress(batch.size)
end
store_results(results)
notify_completion(results.size)
end
private
def process_batch(urls)
hydra = Typhoeus::Hydra.new(max_concurrency: @concurrency)
results = Concurrent::Array.new
urls.each_with_index do |url, idx|
request = build_request(url, idx)
request.on_complete do |response|
results << {
url: url,
status: response.code,
body: response.body,
success: response.success?
}
end
hydra.queue(request)
end
hydra.run
results
end
def build_request(url, index)
session = "job-#{job_id}-req-#{index}"
username = "#{ENV['PROXYHAT_USERNAME']}-country-#{@country}-session-#{session}"
proxy_url = "http://#{username}:#{ENV['PROXYHAT_PASSWORD']}@gate.proxyhat.com:8080"
Typhoeus::Request.new(
url,
method: :get,
proxy: proxy_url,
timeout: 30,
connecttimeout: 15,
followlocation: true,
ssl_verifypeer: true
)
end
def update_progress(count)
# Atualiza cache/Redis com progresso
Rails.cache.increment("scraping:#{job_id}:processed", count)
end
def store_results(results)
# Bulk insert no banco
ScrapedPage.insert_all(
results.select { |r| r[:success] }.map do |r|
{
url: r[:url],
content: r[:body],
scraped_at: Time.current,
job_id: job_id
}
end
)
end
def notify_completion(count)
ScrapingChannel.broadcast_to(
'notifications',
{ event: 'scraping_complete', job_id: job_id, pages: count }
)
end
end
# Uso: enfileira scraping de 1000 URLs
ScrapingJob.perform_later(
1000.times.map { |i| "https://example.com/page/#{i}" },
country: 'BR',
concurrency: 100
)
Comparação: Net::HTTP vs Typhoeus vs ProxyHat SDK
| Característica | Net::HTTP | Typhoeus | ProxyHat SDK |
|---|---|---|---|
| Dependências | Stdlib (nenhuma) | gem typhoeus, libcurl | Net::HTTP wrapper |
| Requisições Paralelas | Manual (threads) | Nativo (Hydra) | Manual (threads) |
| Performance | Moderada | Alta (libcurl) | Moderada |
| Rotação de IP | Manual | Manual | Automática |
| Geo-Targeting | Manual | Manual | Integrado |
| Retry/Circuit Breaker | Implementar | Parcial | Integrado |
| Ideal para | Scripts simples, sem deps | Scraping em escala | Produção rápida |
Pontos-Chave
- Net::HTTP é suficiente para projetos simples sem dependências externas, mas requer mais código para paralelismo.
- Typhoeus com Hydra é a melhor escolha para scraping em escala, oferecendo requisições paralelas nativas.
- ProxyHat suporta rotação por requisição (sem session) ou sticky sessions (com session ID no username).
- Geo-targeting é configurado via username:
user-country-BR-city-sao-paulo.- TLS/SSL requer atenção especial ao usar proxies — configure verify_mode e versões TLS explicitamente.
- Rails integration via Faraday middleware e ActiveJob permite scraping robusto em background.
Para projetos de scraping em produção, recomendamos Typhoeus para alta performance ou o ProxyHat SDK para desenvolvimento rápido com rotação automática. Confira nossos casos de uso de web scraping para mais exemplos práticos.






