Net::HTTP

Ruby標準ライブラリのHTTPクライアント。Ruby 1.8から提供される歴史あるライブラリで、レガシーコードベースで広く使用。基本的なHTTP/HTTPS通信、認証、Cookie処理機能を提供。外部依存関係不要だが、APIが古く使いにくいとされる。

概要

Net::HTTPは、Rubyの標準ライブラリに含まれるHTTPクライアントです。外部の依存関係なしでHTTP/HTTPSリクエストを実行でき、基本的な認証、ファイルアップロード、SSL/TLS通信など、HTTPプロトコルの主要な機能をサポートしています。

主な特徴

  • 標準ライブラリ: Rubyに標準で含まれており、追加インストール不要
  • 完全なHTTPサポート: GET、POST、PUT、DELETE等の全HTTPメソッドに対応
  • HTTPS/SSL対応: セキュアな通信をネイティブサポート
  • 基本認証・ダイジェスト認証: RFC2617準拠の認証機能
  • マルチパートフォームデータ: ファイルアップロードのサポート
  • リダイレクトハンドリング: 自動・手動でのリダイレクト処理
  • Keep-Alive: 接続の再利用による性能向上

インストール

Net::HTTPはRubyの標準ライブラリのため、追加のインストールは不要です。

require 'net/http'
require 'uri'
require 'json'  # JSON処理用(オプション)

基本的な使い方

シンプルなGETリクエスト

require 'net/http'
require 'uri'

uri = URI('https://api.example.com/data')
response = Net::HTTP.get_response(uri)

puts response.code  # => "200"
puts response.body  # レスポンスボディ

より詳細な制御

uri = URI('https://api.example.com/data')

Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  request = Net::HTTP::Get.new(uri)
  response = http.request(request)
  
  puts response.body
end

実装例

1. 各種HTTPメソッドの実装

require 'net/http'
require 'uri'
require 'json'

class HttpClient
  # GETリクエスト
  def self.get(url, headers = {})
    uri = URI(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == 'https'
    
    request = Net::HTTP::Get.new(uri)
    headers.each { |key, value| request[key] = value }
    
    response = http.request(request)
    handle_response(response)
  end
  
  # POSTリクエスト(JSON)
  def self.post_json(url, data, headers = {})
    uri = URI(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == 'https'
    
    request = Net::HTTP::Post.new(uri)
    request['Content-Type'] = 'application/json'
    headers.each { |key, value| request[key] = value }
    request.body = data.to_json
    
    response = http.request(request)
    handle_response(response)
  end
  
  # PUTリクエスト
  def self.put(url, data, headers = {})
    uri = URI(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == 'https'
    
    request = Net::HTTP::Put.new(uri)
    request['Content-Type'] = 'application/json'
    headers.each { |key, value| request[key] = value }
    request.body = data.to_json
    
    response = http.request(request)
    handle_response(response)
  end
  
  # DELETEリクエスト
  def self.delete(url, headers = {})
    uri = URI(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == 'https'
    
    request = Net::HTTP::Delete.new(uri)
    headers.each { |key, value| request[key] = value }
    
    response = http.request(request)
    handle_response(response)
  end
  
  private
  
  def self.handle_response(response)
    case response
    when Net::HTTPSuccess
      { status: response.code, body: response.body }
    when Net::HTTPRedirection
      { status: response.code, location: response['location'] }
    else
      { status: response.code, error: response.message, body: response.body }
    end
  end
end

# 使用例
result = HttpClient.get('https://api.example.com/users/1')
puts result[:body]

2. 認証の実装

# 基本認証
def basic_auth_request(url, username, password)
  uri = URI(url)
  
  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
    request = Net::HTTP::Get.new(uri)
    request.basic_auth(username, password)
    
    response = http.request(request)
    response.body
  end
end

# Bearer トークン認証
def bearer_auth_request(url, token)
  uri = URI(url)
  
  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
    request = Net::HTTP::Get.new(uri)
    request['Authorization'] = "Bearer #{token}"
    
    response = http.request(request)
    response.body
  end
end

# APIキー認証(ヘッダー)
def api_key_auth_request(url, api_key)
  uri = URI(url)
  
  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
    request = Net::HTTP::Get.new(uri)
    request['X-API-Key'] = api_key
    
    response = http.request(request)
    response.body
  end
end

3. ファイルアップロード

require 'net/http/post/multipart'  # gem install multipart-post が必要

# 方法1: Ruby 2.7以降の set_form を使用
def upload_file_modern(url, file_path, additional_params = {})
  uri = URI(url)
  
  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
    request = Net::HTTP::Post.new(uri)
    
    form_data = additional_params.map { |k, v| [k.to_s, v] }
    form_data << ['file', File.open(file_path)]
    
    request.set_form(form_data, 'multipart/form-data')
    response = http.request(request)
    
    response.body
  end
end

# 方法2: 手動でマルチパートフォームを構築
def upload_file_manual(url, file_path, field_name = 'file')
  uri = URI(url)
  boundary = "RubyMultipartPost-#{Time.now.to_i}"
  
  post_body = []
  post_body << "--#{boundary}\r\n"
  post_body << "Content-Disposition: form-data; name=\"#{field_name}\"; filename=\"#{File.basename(file_path)}\"\r\n"
  post_body << "Content-Type: #{content_type_for(file_path)}\r\n"
  post_body << "\r\n"
  post_body << File.read(file_path)
  post_body << "\r\n--#{boundary}--\r\n"
  
  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
    request = Net::HTTP::Post.new(uri)
    request.body = post_body.join
    request["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
    
    response = http.request(request)
    response.body
  end
end

def content_type_for(file_path)
  case File.extname(file_path).downcase
  when '.jpg', '.jpeg' then 'image/jpeg'
  when '.png' then 'image/png'
  when '.gif' then 'image/gif'
  when '.pdf' then 'application/pdf'
  when '.txt' then 'text/plain'
  else 'application/octet-stream'
  end
end

4. エラーハンドリングとリトライ

class ResilientHttpClient
  MAX_RETRIES = 3
  RETRY_DELAY = 1  # 秒
  
  def self.request_with_retry(url, method = :get, options = {})
    uri = URI(url)
    retries = 0
    
    begin
      Net::HTTP.start(uri.host, uri.port, 
                      use_ssl: uri.scheme == 'https',
                      open_timeout: 5,
                      read_timeout: 10) do |http|
        
        request = create_request(method, uri, options)
        response = http.request(request)
        
        case response
        when Net::HTTPSuccess
          return { success: true, data: parse_response(response) }
        when Net::HTTPRedirection
          # リダイレクトを追跡
          return request_with_retry(response['location'], method, options)
        else
          raise HTTPError.new(response.code, response.message)
        end
      end
      
    rescue => e
      retries += 1
      
      if retries <= MAX_RETRIES
        sleep(RETRY_DELAY * retries)  # 指数バックオフ
        retry
      else
        return { success: false, error: e.message }
      end
    end
  end
  
  private
  
  def self.create_request(method, uri, options)
    request_class = case method
    when :get then Net::HTTP::Get
    when :post then Net::HTTP::Post
    when :put then Net::HTTP::Put
    when :delete then Net::HTTP::Delete
    else raise ArgumentError, "Unsupported method: #{method}"
    end
    
    request = request_class.new(uri)
    
    # ヘッダーの設定
    options[:headers]&.each { |k, v| request[k] = v }
    
    # ボディの設定
    if options[:body]
      request.body = options[:body].is_a?(String) ? options[:body] : options[:body].to_json
      request['Content-Type'] = 'application/json' unless request['Content-Type']
    end
    
    # 認証の設定
    if options[:basic_auth]
      request.basic_auth(options[:basic_auth][:username], options[:basic_auth][:password])
    end
    
    request
  end
  
  def self.parse_response(response)
    content_type = response['Content-Type']
    
    if content_type&.include?('application/json')
      JSON.parse(response.body)
    else
      response.body
    end
  end
  
  class HTTPError < StandardError
    attr_reader :code, :message
    
    def initialize(code, message)
      @code = code
      @message = message
      super("HTTP Error #{code}: #{message}")
    end
  end
end

# 使用例
result = ResilientHttpClient.request_with_retry(
  'https://api.example.com/data',
  :post,
  headers: { 'Authorization' => 'Bearer token' },
  body: { name: 'Test', value: 123 }
)

5. ストリーミングとプログレス

# 大きなファイルのダウンロード
def download_file_with_progress(url, destination)
  uri = URI(url)
  
  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
    request = Net::HTTP::Get.new(uri)
    
    http.request(request) do |response|
      total_size = response['Content-Length'].to_i
      downloaded = 0
      
      File.open(destination, 'wb') do |file|
        response.read_body do |chunk|
          file.write(chunk)
          downloaded += chunk.size
          
          # 進捗を表示
          progress = (downloaded.to_f / total_size * 100).round(2)
          print "\rダウンロード中: #{progress}%"
        end
      end
      
      puts "\nダウンロード完了!"
    end
  end
end

# ストリーミングレスポンスの処理
def stream_response(url)
  uri = URI(url)
  
  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
    request = Net::HTTP::Get.new(uri)
    
    http.request(request) do |response|
      response.read_body do |chunk|
        # チャンクごとに処理
        process_chunk(chunk)
      end
    end
  end
end

6. 並列リクエスト

require 'thread'

class ParallelHttpClient
  def self.fetch_multiple(urls, max_threads = 5)
    queue = Queue.new
    urls.each { |url| queue << url }
    
    results = {}
    mutex = Mutex.new
    
    threads = Array.new(max_threads) do
      Thread.new do
        while url = queue.pop(true) rescue nil
          begin
            response = fetch_single(url)
            mutex.synchronize { results[url] = response }
          rescue => e
            mutex.synchronize { results[url] = { error: e.message } }
          end
        end
      end
    end
    
    threads.each(&:join)
    results
  end
  
  private
  
  def self.fetch_single(url)
    uri = URI(url)
    response = Net::HTTP.get_response(uri)
    
    {
      status: response.code,
      body: response.body,
      headers: response.to_hash
    }
  end
end

# 使用例
urls = [
  'https://api.example.com/users/1',
  'https://api.example.com/users/2',
  'https://api.example.com/users/3'
]

results = ParallelHttpClient.fetch_multiple(urls)

他のライブラリとの比較

Net::HTTP vs HTTParty

  • Net::HTTP: 標準ライブラリ、詳細な制御が可能、やや冗長な記述
  • HTTParty: よりシンプルなAPI、便利なヘルパーメソッド

Net::HTTP vs Faraday

  • Net::HTTP: 依存関係なし、基本的な機能
  • Faraday: ミドルウェアシステム、アダプター切り替え可能

Net::HTTP vs RestClient

  • Net::HTTP: 低レベルな制御、標準ライブラリ
  • RestClient: 高レベルな抽象化、使いやすいAPI

ベストプラクティス

  1. 接続の再利用

    # Keep-Aliveを使用した接続の再利用
    Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
      http.keep_alive_timeout = 20
      
      # 複数のリクエストで同じ接続を使用
      response1 = http.request(Net::HTTP::Get.new('/api/v1/users'))
      response2 = http.request(Net::HTTP::Get.new('/api/v1/posts'))
    end
    
  2. タイムアウトの設定

    http = Net::HTTP.new(uri.host, uri.port)
    http.open_timeout = 5    # 接続タイムアウト
    http.read_timeout = 10   # 読み取りタイムアウト
    http.write_timeout = 10  # 書き込みタイムアウト
    
  3. SSL証明書の検証

    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_PEER  # デフォルト
    # 開発環境でのみ(本番環境では使用しない)
    # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    

まとめ

Net::HTTPは、Rubyの標準HTTPクライアントライブラリとして、外部依存なしで幅広いHTTP機能を提供します。やや冗長な記述が必要な場合もありますが、詳細な制御が可能で、小規模から中規模のプロジェクトに適しています。より高度な機能や簡潔なAPIが必要な場合は、HTTPartyやFaradayなどのサードパーティライブラリの使用も検討してください。