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
- Ruby HTTP Clients — compare Net::HTTP with Faraday, HTTParty, and RestClient
- JSON and XML in Ruby — parse and generate common API payload formats
- Working with IO and files in Ruby — useful when you need to save response bodies locally
- Net::HTTP docs (ruby-doc.org)