Net::HTTP
Net::HTTP is Ruby’s standard library for making HTTP requests. It supports all common HTTP methods (GET, POST, PUT, DELETE, etc.), handles response bodies, manages connections, and works with HTTPS through TLS.
The library is a good choice when you want a standard dependency with predictable behavior. It is not the fanciest HTTP client in Ruby, but it is available everywhere and gives you enough control for APIs, simple integrations, and small internal tools. If you need retries, timeouts, or custom connection handling, you can still build on top of it without bringing in a larger abstraction.
That balance is what makes Net::HTTP useful in examples and production code alike. The API is small enough to learn quickly, yet still exposes the pieces that matter when you need to inspect status codes, headers, and bodies directly. The examples below move from a simple request to a slightly more realistic client so the moving parts stay easy to follow.
Loading the library
require 'net/http'
require 'uri'
require 'json' # for parsing API responses
Basic usage
The core workflow for Net::HTTP is: build a URI, send a request, and inspect the response. The GET pattern below is the shortest path to a working request, and it is a good starting point before adding headers, bodies, or custom connection settings.
GET request
The simplest way to make a GET request:
uri = URI('https://api.example.com/data')
response = Net::HTTP.get_response(uri)
response.body # => "{\"status\":\"ok\"}"
response.code # => "200"
response.message # => "OK"
This style is enough for one-off lookups and small scripts, especially when you only need the body or status code. For anything more involved, it helps to create a request object so the headers and method are explicit.
POST request
When you need to send data to the server, the request object form lets you attach a body and custom headers in a way that stays readable even as the payload grows:
uri = URI('https://api.example.com/create')
request = Net::HTTP::Post.new(uri)
request.body = JSON.dump({ name: 'Alice', email: 'alice@example.com' })
request['Content-Type'] = 'application/json'
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
response.code # => "201"
The request object form is a little longer, but it scales better when the request needs a body, custom headers, or reusable setup. That extra explicitness is usually worth it once the request stops being a one-line example.
Constructor parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
address | String | required | Hostname or IP address |
port | Integer | 80 (HTTP), 443 (HTTPS) | Port number |
proxy_addr | String | nil | Proxy server address |
proxy_port | Integer | nil | Proxy server port |
open_timeout | Integer | nil | Connection timeout (seconds) |
read_timeout | Integer | nil | Read timeout (seconds) |
use_ssl | Boolean | false | Enable HTTPS |
Common methods
Net::HTTP.get_response(uri)
Synchronous GET request. Returns a Net::HTTPResponse object.
response = Net::HTTP.get_response(URI('https://example.com'))
This is the quickest path when the request is simple and you only need the response object. It keeps the example compact while still showing the standard URI-based workflow. For many basic scripts, this single method call is all you need to fetch data and move on.
Net::HTTP.post(uri, body, headers)
Synchronous POST request.
response = Net::HTTP.post(
URI('https://api.example.com/login'),
'username=admin&password=secret',
{ 'Content-Type' => 'application/x-www-form-urlencoded' }
)
Use this form when the payload is small and you do not need to manage the connection yourself. It is a practical fit for form posts and basic API calls. The convenience methods handle the connection lifecycle behind the scenes, so you can focus on the request content instead of socket management.
Net::HTTP.start(host, port, use_ssl: false) { |http| ... }
Opens a connection and yields the HTTP object. The connection closes automatically when the block finishes.
Net::HTTP.start('api.example.com', 443, use_ssl: true) do |http|
request = Net::HTTP::Get.new('/users')
response = http.request(request)
JSON.parse(response.body)
end
The block form opens the connection for the duration of the request and then closes it automatically. That pattern is a good default when you want to keep the connection lifecycle obvious and contained. It also avoids the common mistake of forgetting to close a manually managed connection, which can lead to resource leaks in long-running processes.
Net::HTTP.new(address, port)
Creates a new HTTP object without connecting. Use start to open the connection.
http = Net::HTTP.new('api.example.com', 443)
http.use_ssl = true
http.start
response = http.get('/data')
http.finish
This version is useful when the same connection object needs to be reused across several requests. It gives you more control at the cost of a little more setup. When you need to make several requests to the same host, keeping the connection open avoids the overhead of a new TLS handshake for each call.
Request types
The library provides request classes for every standard HTTP method. Each one follows the same constructor pattern, so switching from GET to POST or PATCH is just a matter of changing the class name:
# All HTTP methods are supported
Net::HTTP::Get.new(uri)
Net::HTTP::Post.new(uri)
Net::HTTP::Put.new(uri)
Net::HTTP::Delete.new(uri)
Net::HTTP::Patch.new(uri)
Net::HTTP::Head.new(uri)
Net::HTTP::Options.new(uri)
Handling responses
Once you have a response object, you can inspect the status code, read the body, and iterate over the headers. The response class hierarchy includes subclasses for each status range, so checking the class is a safe way to branch on success vs. failure:
response = Net::HTTP.get_response(URI('https://api.example.com/data'))
# Check status code
if response.code == '200'
data = JSON.parse(response.body)
end
# Work with headers
response['Content-Type'] # => "application/json"
response['X-Request-Id'] # => "abc123"
# Iterate over all headers
response.each_header do |name, value|
puts "#{name}: #{value}"
end
Checking the response class and headers before parsing keeps the failure mode easy to spot. It also makes the code more honest about what it expects from the server, which is especially helpful when the endpoint can return several status codes.
Working with JSON APIs
require 'net/http'
require 'uri'
require 'json'
def fetch_json(url)
uri = URI(url)
response = Net::HTTP.get_response(uri)
case response
when Net::HTTPSuccess
JSON.parse(response.body, symbolize_names: true)
else
raise "HTTP Error: #{response.code} #{response.message}"
end
end
users = fetch_json('https://api.example.com/users')
When you are using Net::HTTP in production code, the main habit to keep is explicitness. Set the timeout values you need, check the response class before parsing, and treat the HTTP status code as part of the contract. That keeps the client code easy to reason about and helps failures show up in a place where you can actually act on them.
If the request flow starts to repeat, wrap the setup in a small helper instead of expanding every call site. That keeps the real behavior close to the request and avoids burying the important details under repeated connection setup.
See Also
json— parse and generate JSON data (commonly used with Net::HTTP)yaml— alternative serialization format for configuration and datahash-fetch— safely access hash values when working with response data