rubyguides

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

ParameterTypeDefaultDescription
addressStringrequiredHostname or IP address
portInteger80 (HTTP), 443 (HTTPS)Port number
proxy_addrStringnilProxy server address
proxy_portIntegernilProxy server port
open_timeoutIntegernilConnection timeout (seconds)
read_timeoutIntegernilRead timeout (seconds)
use_sslBooleanfalseEnable 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 data
  • hash-fetch — safely access hash values when working with response data