rubyguides

Making HTTP Requests in Ruby with Net::HTTP

Ruby comes with a powerful standard library for making HTTP requests: Net::HTTP. Whether you are fetching data from an API, downloading files, or sending data to a webhook, Net::HTTP has you covered. No external gems required.

This guide walks you through common HTTP operations in Ruby, perfect for automation scripts and DevOps tasks.

TL;DR

Use Net::HTTP from the standard library to make HTTP requests without adding gems. Create a connection, enable SSL for HTTPS, build a request object with headers and a body, then call http.request. Always set timeouts and check the response code before parsing the body. For complex needs, move up to Faraday or HTTParty.

Setting up your request

Every HTTP request in Net::HTTP starts with creating a connection object. You will typically specify the host and port:

require 'net/http'
require 'uri'

uri = URI('https://api.github.com/users')
http = Net::HTTP.new(uri.host, uri.port)

For HTTPS endpoints you must enable SSL. Without it the connection will fail or send data in the clear. The verify_mode setting determines how your script handles certificate validation: strict checking for production, and permissive checking for local development with self-signed certs.

http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER

The verify_mode setting controls SSL certificate verification. Use VERIFY_PEER for production to validate certificates, or VERIFY_NONE for development with self-signed certificates. Skipping verification in production exposes you to man-in-the-middle attacks, so always keep it on for any code that touches real data.

Making GET requests

GET is the most common HTTP method, used to retrieve data. Here is how to fetch data from an API:

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

uri = URI('https://api.github.com/users/ruby')
response = Net::HTTP.get_response(uri)

puts response.code        # => "200"
puts response.message    # => "OK"
puts response.body       # => JSON string with user data

Net::HTTP.get_response is a convenience shortcut that creates the request automatically. For adding headers or fine-tuned control over the request object, reach for Net::HTTP::Get.new directly. The explicit form is longer but gives you full access to headers, timeouts, and the connection object.

require 'net/http'
require 'uri'

uri = URI('https://api.github.com/users/ruby')
request = Net::HTTP::Get.new(uri)

request['Accept'] = 'application/vnd.github.v3+json'
response = http.request(request)

data = JSON.parse(response.body)
puts data['login']    # => "ruby"

Making POST requests

POST requests send data to a server. They are useful for creating resources, submitting forms, or sending JSON to an API. The key difference from GET is the request body: you attach the data you want the server to process, and you set the Content-Type header so the server knows how to interpret it.

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

uri = URI('https://httpbin.org/post')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = JSON.generate({ name: 'Ruby', type: 'language' })

response = http.request(request)
puts response.code    # => "200"
puts response.body    # => Echoed JSON response

The request object gives you full control over the HTTP method, headers, and body. Use JSON.generate to convert a Ruby hash to a JSON string before assigning it to request.body. The server echoes back whatever you sent, which makes httpbin.org a helpful endpoint for testing and debugging your request payloads.

Working with headers

Headers provide metadata about your request. Common headers include authorization tokens, content type declarations, and custom application-specific values. Most REST APIs require at least one header for authentication.

request = Net::HTTP::Get.new(uri)

# Authentication
request['Authorization'] = 'Bearer YOUR_TOKEN'

# Content type
request['Content-Type'] = 'application/json'

# Accept response format
request['Accept'] = 'application/json'

# Custom headers
request['X-Custom-Header'] = 'MyValue'

response = http.request(request)

Headers are case-insensitive in HTTP, but Ruby uses the capitalization you provide. Stick to the standard formats shown above for compatibility. Sending the wrong Content-Type header can cause the server to reject or misinterpret your request body, so double-check that the header matches the actual format of the data you are sending.

Handling responses

Net::HTTP provides several ways to work with responses. After calling http.request, you get back a response object with the status code, message, headers, and body. Always check the status before you try to use the body content.

response = http.request(request)

# Check status code
if response.code == '200'
  puts 'Success!'
end

# Parse JSON response
data = JSON.parse(response.body)

# Read response header
puts response['Content-Type']    # => "application/json"

# Response object methods
puts response.content_type
puts response.content_length
puts response['Set-Cookie']

The response object provides access to the status code via code as a string or integer_code as an integer. The message method returns the human-readable status phrase like “OK” or “Not Found”. Access individual headers with bracket notation just like a hash, and use content_type and content_length as shortcuts for the most common header lookups.

Error handling

Network requests can fail in many ways. Servers go down, timeouts expire, SSL certificates fail validation, and DNS lookups can time out. Always wrap your HTTP calls in exception handlers to keep your script from crashing on transient network problems.

require 'net/http'
require 'uri'

def fetch_url(url)
  uri = URI(url)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  http.open_timeout = 5    # Connection timeout
  http.read_timeout = 10   # Read timeout
  
  request = Net::HTTP::Get.new(uri)
  
  begin
    response = http.request(request)
    case response
    when Net::HTTPSuccess
      response.body
    when Net::HTTPRedirection
      # Follow redirect
      fetch_url(response['Location'])
    else
      puts "Error: #{response.code} #{response.message}"
      nil
    end
  rescue Net::OpenTimeout => e
    puts "Connection timeout: #{e.message}"
    nil
  rescue Net::ReadTimeout => e
    puts "Read timeout: #{e.message}"
    nil
  rescue OpenSSL::SSL::SSLError => e
    puts "SSL error: #{e.message}"
    nil
  rescue SocketError => e
    puts "DNS or connection error: #{e.message}"
    nil
  end
end

Key timeouts to set are open_timeout, the seconds to wait when establishing a connection, and read_timeout, the seconds to wait for data once connected. Production scripts should set both. Without explicit timeouts, a slow or unresponsive server can hang your script indefinitely.

Practical examples

Downloading a file

The simplest Net::HTTP pattern is a single GET request that writes the response body to disk. This is enough for small files like configuration data or JSON payloads where you can hold the entire response in memory at once.

require 'net/http'
require 'uri'

uri = URI('https://example.com/large-file.zip')
response = Net::HTTP.get_response(uri)

if response.code == '200'
  File.write('large-file.zip', response.body)
  puts 'File downloaded successfully!'
end

Checking API availability

For monitoring and health checks, you often just need to know whether an endpoint returns a 200 status. A small predicate method wraps that check in a single callable function that you can drop into scripts and cron jobs.

require 'net/http'
require 'uri'

def api_healthy?(url)
  uri = URI("#{url}/health")
  response = Net::HTTP.get_response(uri)
  response.code == '200'
end

puts api_healthy?('https://api.example.com')    # => true or false

Sending webhook notifications

Health checks read data from an API. Webhooks do the opposite: they push data to another service. When your script finishes a deployment or a data pipeline step, a POST request with a JSON payload tells the downstream system what happened. This pattern appears in CI pipelines, deployment scripts, and monitoring tools everywhere.

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

def send_webhook(url, payload)
  uri = URI(url)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  
  request = Net::HTTP::Post.new(uri)
  request['Content-Type'] = 'application/json'
  request.body = JSON.generate(payload)
  
  http.request(request)
end

send_webhook('https://hooks.example.com/notify', 
  { event: 'deploy', status: 'success', service: 'my-app' })

Query string parameters

Many APIs accept parameters in the URL query string. Instead of building the query string by hand, use URI.encode_www_form to encode a hash of parameters correctly, including proper handling of special characters and multiple values for the same key.

require 'net/http'
require 'uri'

# Build URL with query parameters
params = { page: 1, per_page: 50, sort: 'desc' }
uri = URI('https://api.example.com/items')
uri.query = URI.encode_www_form(params)

response = Net::HTTP.get_response(uri)

When to use Net::HTTP

Net::HTTP is ideal for simple API calls in scripts, automation and DevOps tasks, learning how HTTP works at a low level, and lightweight integrations with no dependencies. It ships with Ruby so there is nothing to install.

Here is a minimal pattern you can adapt for most scripts:

require 'net/http'
require 'json'

def api_get(path, token: nil)
  uri = URI(path)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  http.open_timeout = 5
  req = Net::HTTP::Get.new(uri)
  req['Authorization'] = "Bearer #{token}" if token
  res = http.request(req)
  res.is_a?(Net::HTTPSuccess) ? JSON.parse(res.body) : nil
end

For more complex needs, consider these gems:

  • Faraday for a unified interface with multiple adapters including Net::HTTP, HTTPClient, and Patron
  • HTTParty for a convenient DSL with automatic JSON parsing
  • RestClient for a simple Ruby-like HTTP client interface

Frequently asked questions

Do I need a gem for basic HTTP requests?

No. Net::HTTP ships with Ruby, so you can make requests without adding anything to your Gemfile. That makes it handy for small scripts, build tooling, and quick integrations where you want to keep dependencies to a minimum.

When should I switch from Net::HTTP to Faraday or HTTParty?

Switch when your code starts repeating the same setup logic across multiple files, when you need retries or middleware like logging, or when you want a cleaner abstraction for a larger application. Net::HTTP is great for clarity but is not always the most ergonomic choice for complex use cases.

How do I handle redirects with Net::HTTP?

Check the response class with when Net::HTTPRedirection and recursively call your fetch method with the Location header value. The error handling example above shows this pattern. For more advanced redirect handling with a redirect limit, wrap the recursion in a counter.

Why making HTTP requests matters in Ruby scripts

Making HTTP requests is one of the most common tasks in Ruby automation, and Net::HTTP handles it without external dependencies. It is part of the standard library, gives you direct control over requests and responses, and teaches you HTTP mechanics clearly. Once you understand the basics, you can decide whether the simplicity is enough or whether a higher-level client would save time in your application.

When in doubt, start making HTTP requests with Net::HTTP, add explicit timeouts, check response codes, and keep your request objects small and readable. Those habits make the code easier to debug and safer to run in production.

See Also