Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions examples/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env ruby

require_relative "../lib/wreq"

begin
Wreq.get("not-a-valid-url")
rescue Wreq::Error => error
puts "URI: #{error.uri.inspect}"
puts "Status: #{error.status.inspect}"
puts "#{error.class}: #{error.message}"
puts "Native builder error: #{error.is_builder}"
end
109 changes: 93 additions & 16 deletions lib/wreq_ruby/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,86 @@

unless defined?(Wreq)
module Wreq
# Base class for regular errors raised by wreq-ruby.
#
# This remains a RuntimeError so existing broad rescue handlers continue
# to work. Predicate methods describe a captured native wreq::Error rather
# than the Ruby exception class, and more than one predicate may be true.
# Errors created entirely by the binding return false for every predicate.
class Error < RuntimeError
# The URI associated with the native error.
#
# This explicit accessor may contain credentials or sensitive query
# parameters. Error messages and inspection output omit it, so avoid
# logging this value without redacting it first.
#
# @return [String, nil] frozen URI string, if one was recorded
attr_reader :uri

# The response status associated with the error.
#
# @return [Integer, nil] HTTP status code, if one was recorded
attr_reader :status

# @return [Boolean] whether the native error came from a builder
def is_builder
end

# @return [Boolean] whether the native error came from redirect handling
def is_redirect
end

# @return [Boolean] whether the native error represents an HTTP status
def is_status
end

# @return [Boolean] whether the native error is related to a timeout
def is_timeout
end

# @return [Boolean] whether the native error is related to a request
def is_request
end

# @return [Boolean] whether the native error is related to connecting
def is_connect
end

# @return [Boolean] whether the native error is related to proxy connection
def is_proxy_connect
end

# @return [Boolean] whether the native error is a connection reset
def is_connection_reset
end

# @return [Boolean] whether the native error is related to a body
def is_body
end

# @return [Boolean] whether the native error is related to TLS
def is_tls
end

# @return [Boolean] whether the native error is related to decoding
def is_decode
end

# @return [Boolean] whether the native error is related to an upgrade
def is_upgrade
end
end

# Raised when a native request wait is interrupted.
#
# InterruptError stays outside StandardError so broad application rescues
# do not swallow Ruby interrupts.
class InterruptError < Interrupt; end

# System-level and runtime errors

# Memory allocation failed.
class MemoryError < StandardError; end
class MemoryError < Error; end

# The native extension was inherited from a parent process.
#
Expand All @@ -17,7 +93,7 @@ class MemoryError < StandardError; end
# Process.fork do
# Wreq::Client.new # Raises when the parent loaded wreq-ruby.
# end
class ForkError < RuntimeError; end
class ForkError < Error; end

# Network connection errors

Expand All @@ -32,7 +108,7 @@ class ForkError < RuntimeError; end
# puts "Connection failed: #{e.message}"
# retry_with_backoff
# end
class ConnectionError < StandardError; end
class ConnectionError < Error; end

# Proxy Connection to the server failed.
#
Expand All @@ -44,7 +120,7 @@ class ConnectionError < StandardError; end
# puts "Proxy connection failed: #{e.message}"
# retry_with_different_proxy
# end
class ProxyConnectionError < StandardError; end
class ProxyConnectionError < Error; end

# Connection was reset by the server.
#
Expand All @@ -54,7 +130,7 @@ class ProxyConnectionError < StandardError; end
# rescue Wreq::ConnectionResetError => e
# puts "Connection reset: #{e.message}"
# end
class ConnectionResetError < StandardError; end
class ConnectionResetError < Error; end

# TLS/SSL error occurred.
#
Expand All @@ -67,7 +143,7 @@ class ConnectionResetError < StandardError; end
# rescue Wreq::TlsError => e
# puts "TLS error: #{e.message}"
# end
class TlsError < StandardError; end
class TlsError < Error; end

# HTTP protocol and request/response errors

Expand All @@ -79,20 +155,21 @@ class TlsError < StandardError; end
# rescue Wreq::RequestError => e
# puts "Request failed: #{e.message}"
# end
class RequestError < StandardError; end
class RequestError < Error; end

# HTTP status code indicates an error.
#
# Raised when the server returns an error status code (4xx or 5xx).
# Raised by Response#raise_for_status! for a 4xx or 5xx response. Requests
# continue to return these responses normally until that method is called.
#
# @example
# begin
# response = client.get("https://httpbin.io/status/404")
# response.raise_for_status!
# rescue Wreq::StatusError => e
# puts "HTTP error: #{e.message}"
# # e.response contains the full response
# puts "HTTP #{e.status}: #{e.message}"
# end
class StatusError < StandardError; end
class StatusError < Error; end

# Redirect handling failed.
#
Expand All @@ -105,7 +182,7 @@ class StatusError < StandardError; end
# rescue Wreq::RedirectError => e
# puts "Too many redirects: #{e.message}"
# end
class RedirectError < StandardError; end
class RedirectError < Error; end

# Request timed out.
#
Expand All @@ -119,7 +196,7 @@ class RedirectError < StandardError; end
# puts "Request timed out: #{e.message}"
# retry_with_longer_timeout
# end
class TimeoutError < StandardError; end
class TimeoutError < Error; end

# Data processing and encoding errors

Expand All @@ -131,7 +208,7 @@ class TimeoutError < StandardError; end
# rescue Wreq::BodyError => e
# puts "Body error: #{e.message}"
# end
class BodyError < StandardError; end
class BodyError < Error; end

# Decoding response failed.
#
Expand All @@ -147,7 +224,7 @@ class BodyError < StandardError; end
# # Fall back to binary data
# data = response.body
# end
class DecodingError < StandardError; end
class DecodingError < Error; end

# Configuration and builder errors

Expand All @@ -162,6 +239,6 @@ class DecodingError < StandardError; end
# rescue Wreq::BuilderError => e
# puts "Invalid configuration: #{e.message}"
# end
class BuilderError < StandardError; end
class BuilderError < Error; end
end
end
12 changes: 12 additions & 0 deletions lib/wreq_ruby/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ def code
def status
end

# Raise for a client or server error status.
#
# This check is opt-in and does not consume the response body.
#
# @return [Wreq::Response] This response for non-error statuses
# @raise [Wreq::StatusError] for a 4xx or 5xx status
# @example
# response = client.get("https://example.com/missing")
# response.raise_for_status!
def raise_for_status!
end

# Get the HTTP protocol version used.
#
# @return [Wreq::Version] HTTP version (HTTP/1.1, HTTP/2, etc.)
Expand Down
26 changes: 25 additions & 1 deletion src/client/resp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use bytes::Bytes;
use futures_util::TryFutureExt;
use http::{Extensions, HeaderMap, response::Response as HttpResponse};
use http_body_util::BodyExt;
use magnus::{Error, Module, RArray, RModule, Ruby, Value, scan_args::scan_args};
use magnus::{Error, Module, RArray, RModule, Ruby, Value, scan_args::scan_args, typed_data::Obj};
use wreq::Uri;

use crate::{
Expand Down Expand Up @@ -63,6 +63,14 @@ impl Response {
}
}

/// Build a body-free native response for its status classification logic.
fn response_for_status(&self) -> wreq::Response {
let mut response = HttpResponse::new(Bytes::new());
*response.status_mut() = self.status.0;
*response.extensions_mut() = self.extensions.clone();
wreq::Response::from(response)
}

/// Internal method to get the wreq::Response, optionally streaming the body.
fn response(&self, ruby: &Ruby, stream: bool) -> Result<wreq::Response, Error> {
rt::ensure_current(ruby)?;
Expand Down Expand Up @@ -123,6 +131,18 @@ impl Response {
self.status
}

/// Return this response unless its status is a client or server error.
///
/// Delegates classification to `wreq::Response::error_for_status_ref`
/// without consuming the response body.
pub fn raise_for_status(ruby: &Ruby, rb_self: Obj<Self>) -> Result<Obj<Self>, Error> {
rb_self
.response_for_status()
.error_for_status_ref()
.map_err(|err| wreq_error(ruby, err))?;
Ok(rb_self)
}

/// Get the response HTTP version.
#[inline]
pub fn version(&self) -> Version {
Expand Down Expand Up @@ -230,6 +250,10 @@ pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
let response = gem_module.define_class("Response", ruby.class_object())?;
response.define_method("code", magnus::method!(Response::code, 0))?;
response.define_method("status", magnus::method!(Response::status, 0))?;
response.define_method(
"raise_for_status!",
magnus::method!(Response::raise_for_status, 0),
)?;
response.define_method("version", magnus::method!(Response::version, 0))?;
response.define_method("url", magnus::method!(Response::url, 0))?;
response.define_method(
Expand Down
Loading