skip to content

In Ruby, what does URI.open from open-uri return for an https URL, and how does it treat redirects and error statuses differently from Net::HTTP?

level: juniorimportance: nice to knowfreq 22%

answer

  1. a file-like object, not a String
  2. StringIO, or Tempfile past 10 KB
  3. OpenURI::HTTPError on non-2xx
  4. follows redirects, refuses https to http
  5. Kernel#open no longer patched since 3.0

basics

~10 s

URI.open returns a file-like object, a StringIO or a Tempfile for larger bodies, extended with status and content_type readers. Unlike Net::HTTP it follows redirects and raises OpenURI::HTTPError for any non-2xx status.

solid answer

~40 s

`require "open-uri"` adds `URI.open`, a thin wrapper over Net::HTTP that reads a URL like a file. It returns an IO-like object: a `StringIO` for bodies up to 10,240 bytes and a `Tempfile` above that, extended with `OpenURI::Meta`, so you get `status` (for example `["200", "OK"]`), `content_type`, `charset` and `base_uri`, the final URL. With a block it yields that object and returns the block's value. Unlike Net::HTTP it follows 301, 302, 303, 307 and 308 redirects (up to 64 by default) but refuses an https to http downgrade, and any other non-2xx status raises `OpenURI::HTTPError`, whose `io` holds the error body. Options include `read_timeout:`, `open_timeout:` and String keys for headers. Since Ruby 3.0 open-uri no longer redefines `Kernel#open`, so call `URI.open`.

code

ruby · 13 lines
ruby
require "open-uri"

url = "https://api.example.com/v1/forecast?city=Oslo"

begin
  URI.open(url, "User-Agent" => "forecast-bot/1.0", read_timeout: 5) do |f|
    puts f.status.inspect   # => ["200", "OK"]
    puts f.content_type
    puts f.read
  end
rescue OpenURI::HTTPError => e
  warn "forecast failed: #{e.io.status.join(" ")}"
end

go deeper

for a junior

Recall that require "open-uri" gives URI.open, which reads a URL like a file, and that it raises OpenURI::HTTPError when the status is not 2xx.

for a middle

Explain the StringIO versus Tempfile result, OpenURI::Meta readers, redirect rules and how String versus Symbol option keys differ.

for a senior

Choose open-uri for simple reads only, set its timeouts, and replace legacy open(url) calls left over from before Ruby 3.0.

for a principal

Set a guideline on when scripts may use open-uri and when services must go through a shared, instrumented HTTP client.

## What open-uri is **open-uri** is a default gem that makes an http, https or ftp URL readable like a local file. It is a wrapper around `Net::HTTP`: under the hood it builds the same connection, request and response, then copies the body into a buffer and hands you something that behaves like an `IO`. ```ruby require "open-uri" body = URI.open("https://api.example.com/v1/forecast?city=Oslo", &:read) ``` ## What URI.open returns The object you get back depends on the body size: - up to **10,240 bytes**, a **`StringIO`**; - beyond that, a **`Tempfile`** opened in binary mode, so a large download does not sit entirely in memory as a String. Either way it is extended with **`OpenURI::Meta`**, which adds readers for the response: | Reader | Example value | |---|---| | `status` | `["200", "OK"]` | | `content_type` | `"application/json"` | | `charset` | `"utf-8"` when the Content-Type names it | | `base_uri` | the final URI after redirects | | `meta` | a Hash of response headers, keys downcased | With a block, `URI.open` yields the object and returns the block's value, which is the tidy form. Without a block you own the object and should `close` it, which matters for the `Tempfile` case. ## Redirects Plain `Net::HTTP` never follows a redirect. `URI.open` does: 1. On 301, 302, 303, 307 or 308 it reads `Location`, resolves a relative value against the current URL and requests the new one. 2. It stops with **`OpenURI::TooManyRedirects`** after `max_redirects` hops, **64** by default, and raises on a loop. 3. It refuses to follow from https to http, or from http to a non-network scheme, raising a `RuntimeError` whose message starts with "redirection forbidden". 4. With `redirect: false` it raises **`OpenURI::HTTPRedirect`** instead, whose `uri` holds the target. It also drops `http_basic_authentication` credentials when it follows a redirect, so they are sent only to the URL you named. ## Error statuses Any status outside 2xx that is not a followed redirect raises **`OpenURI::HTTPError`**, a `StandardError`. Its `io` reader holds the response body and status, so you can still read an API's JSON error document: - `e.io.status` gives `["404", "Not Found"]`; - `e.io.read` gives the body. Compare with Net::HTTP, where `Net::HTTP.get` returns the error body as a String and `get_response` returns a `Net::HTTPNotFound` for you to inspect. ## Options The second argument is an options Hash. Symbol keys are open-uri options; **String keys are request headers**: - `read_timeout:` and `open_timeout:` pass through to the underlying `Net::HTTP` (otherwise its 60-second defaults apply); - `"User-Agent" => "forecast-bot/1.0"` or `"Authorization" => "Bearer ..."` set headers; - `http_basic_authentication: [user, password]`, `ssl_verify_mode:`, `redirect:`, `max_redirects:`, `progress_proc:`. ## Kernel#open and a version note Before Ruby 3.0, `require "open-uri"` also redefined `Kernel#open` so that `open("https://...")` fetched a URL. **Ruby 3.0 removed that redefinition**; call `URI.open` explicitly. `URI.open` still hands a string that does not look like `scheme://...` to `Kernel#open`, which opens a local file. What that means for strings that come from users is a security question in its own right. ## Other entry points and progress hooks `URI.open` is not the only door. Requiring open-uri also adds `open` and `read` to `URI::HTTP`, `URI::HTTPS` and `URI::FTP` objects, so `URI("https://api.example.com/v1/forecast").read` returns the body String directly, still extended with the `OpenURI::Meta` readers. Two options help with large downloads: - `content_length_proc:` is called once with the `Content-Length` value (or nil when the server sends none); - `progress_proc:` is called with the running byte count as chunks arrive, which is enough for a progress bar. ## When to prefer it - Good for scripts and small tools that read one resource and want redirects handled. - Less suited to services that need connection reuse, retries policy or precise control of methods and bodies; Net::HTTP sessions fit those better.

  • In Ruby 4.0, what happens if code calls open("https://api.example.com/") after require "open-uri"?
    Since Ruby 3.0 open-uri no longer patches `Kernel#open`, so `open` treats the String as a local file path and typically raises `Errno::ENOENT`. `URI.open` is the call that fetches a URL.
  • How do you read the JSON error body when URI.open raises OpenURI::HTTPError?
    The exception's `io` reader is the buffered response, extended with `OpenURI::Meta`: `e.io.status` gives the code and message, and `e.io.read` gives the body, which an API often fills with an error document.

saying these in an interview costs you the question

  • URI.open returns the body as a String
  • URI.open returns normally on a 404 and you must check status
  • URI.open follows a redirect from https to http
  • open(url) fetches a URL in Ruby 4.0 once open-uri is required
  • Symbol keys in URI.open's options are sent as headers