Skip to content
Merged
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
158 changes: 158 additions & 0 deletions lib/wreq_ruby/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# frozen_string_literal: true

module Wreq
# Network connection errors

# Connection to the server failed.
#
# Raised when the client cannot establish a connection to the server.
#
# @example
# begin
# client.get("http://localhost:9999")
# rescue Wreq::ConnectionError => e
# puts "Connection failed: #{e.message}"
# retry_with_backoff
# end
unless const_defined?(:ConnectionError)
class ConnectionError < StandardError; end
end

# Connection was reset by the server.
#
# Raised when the server closes the connection unexpectedly.
#
# @example
# rescue Wreq::ConnectionResetError => e
# puts "Connection reset: #{e.message}"
# end
unless const_defined?(:ConnectionResetError)
class ConnectionResetError < StandardError; end
end

# TLS/SSL error occurred.
#
# Raised when there's an error with TLS/SSL, such as certificate
# verification failure or protocol mismatch.
#
# @example
# begin
# client.get("https://self-signed.badssl.com")
# rescue Wreq::TlsError => e
# puts "TLS error: #{e.message}"
# end
unless const_defined?(:TlsError)
class TlsError < StandardError; end
end

# HTTP protocol and request/response errors

# Request failed.
#
# Generic error for request failures that don't fit other categories.
#
# @example
# rescue Wreq::RequestError => e
# puts "Request failed: #{e.message}"
# end
unless const_defined?(:RequestError)
class RequestError < StandardError; end
end

# HTTP status code indicates an error.
#
# Raised when the server returns an error status code (4xx or 5xx).
#
# @example
# begin
# response = client.get("https://httpbin.org/status/404")
# rescue Wreq::StatusError => e
# puts "HTTP error: #{e.message}"
# # e.response contains the full response
# end
unless const_defined?(:StatusError)
class StatusError < StandardError; end
end

# Redirect handling failed.
#
# Raised when too many redirects occur or redirect logic fails.
#
# @example
# begin
# client = Wreq::Client.new(max_redirects: 3)
# client.get("https://httpbin.org/redirect/10")
# rescue Wreq::RedirectError => e
# puts "Too many redirects: #{e.message}"
# end
unless const_defined?(:RedirectError)
class RedirectError < StandardError; end
end

# Request timed out.
#
# Raised when the request exceeds the configured timeout.
#
# @example
# begin
# client = Wreq::Client.new(timeout: 5)
# client.get("https://httpbin.org/delay/10")
# rescue Wreq::TimeoutError => e
# puts "Request timed out: #{e.message}"
# retry_with_longer_timeout
# end
unless const_defined?(:TimeoutError)
class TimeoutError < StandardError; end
end

# Data processing and encoding errors

# Response body processing failed.
#
# Raised when there's an error reading or processing the response body.
#
# @example
# rescue Wreq::BodyError => e
# puts "Body error: #{e.message}"
# end
unless const_defined?(:BodyError)
class BodyError < StandardError; end
end

# Decoding response failed.
#
# Raised when response content cannot be decoded (e.g., invalid UTF-8,
# malformed JSON, corrupted compression).
#
# @example
# begin
# response = client.get("https://example.com/invalid-utf8")
# response.text # May raise DecodingError
# rescue Wreq::DecodingError => e
# puts "Decoding error: #{e.message}"
# # Fall back to binary data
# data = response.body
# end
unless const_defined?(:DecodingError)
class DecodingError < StandardError; end
end

# Configuration and builder errors

# Client configuration is invalid.
#
# Raised when the client is configured with invalid options.
#
# @example
# begin
# client = Wreq::Client.new(
# proxy: "invalid://proxy",
# timeout: -1
# )
# rescue Wreq::BuilderError => e
# puts "Invalid configuration: #{e.message}"
# end
unless const_defined?(:BuilderError)
class BuilderError < StandardError; end
end
end
16 changes: 6 additions & 10 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use wreq::{
header::{HeaderMap, HeaderName, HeaderValue, OrigHeaderMap},
};

use crate::{format_magnus_error, nogvl};
use crate::nogvl;

/// A builder for `Client`.
#[derive(Debug, Default, Deserialize)]
Expand Down Expand Up @@ -84,7 +84,7 @@ struct Builder {
no_proxy: Option<bool>,
/// The proxy to use for the client.
#[serde(skip)]
proxy: Option<Uri>,
proxy: Option<Proxy>,

// ========= Compression options =========
/// Sets gzip as an accepted encoding.
Expand Down Expand Up @@ -165,12 +165,8 @@ impl Builder {
.get(ruby.to_symbol("proxy"))
.and_then(RString::from_value)
{
let uri = Uri::from_maybe_shared(proxy.to_bytes()).map_err(|e| {
magnus::Error::new(
ruby.exception_arg_error(),
format!("invalid proxy URI '{proxy}': {e}"),
)
})?;
let uri = Proxy::all(proxy.to_bytes().as_ref())
.map_err(|err| crate::error::wreq_error_to_magnus(ruby, err))?;
builder.proxy = Some(uri);
}

Expand Down Expand Up @@ -297,7 +293,7 @@ impl Client {
apply_option!(set_if_some, builder, params.verify, cert_verification);

// Network options.
apply_option!(set_if_some_map_ok, builder, params.proxy, proxy, Proxy::all);
apply_option!(set_if_some, builder, params.proxy, proxy);
apply_option!(set_if_true, builder, params.no_proxy, no_proxy, false);

// Compression options.
Expand All @@ -309,7 +305,7 @@ impl Client {
builder
.build()
.map(Client)
.map_err(|e| format_magnus_error(ruby, e))
.map_err(|err| crate::error::wreq_error_to_magnus(ruby, err))
})
} else {
nogvl::nogvl(|| Ok(Self(wreq::Client::new())))
Expand Down
83 changes: 83 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use magnus::{
Error as MagnusError, RModule, Ruby, exception::ExceptionClass, prelude::*, value::Lazy,
};

static WREQ: Lazy<RModule> = Lazy::new(|ruby| ruby.define_module(crate::RUBY_MODULE_NAME).unwrap());
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Potential issue: The WREQ module is being defined here with define_module, but the same module is already defined in lib.rs (line 43). Calling define_module multiple times should be safe in Ruby/Magnus (it returns the existing module if it already exists), but it's redundant. Consider passing the module as a parameter to include() or using find_class instead of define_module to avoid confusion.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback


macro_rules! define_exception {
($name:ident, $ruby_name:literal, $parent_method:ident) => {
static $name: Lazy<ExceptionClass> = Lazy::new(|ruby| {
ruby.get_inner(&WREQ)
.define_error($ruby_name, ruby.$parent_method())
.unwrap()
});
};
}

macro_rules! map_wreq_error {
($ruby:expr, $err:expr, $msg:expr, $($check_method:ident => $exception:ident),* $(,)?) => {
{
$(
if $err.$check_method() {
return MagnusError::new($ruby.get_inner(&$exception), $msg);
}
)*
MagnusError::new($ruby.exception_runtime_error(), $msg)
}
};
}

// Network connection errors
define_exception!(CONNECTION_ERROR, "ConnectionError", exception_runtime_error);
define_exception!(
CONNECTION_RESET_ERROR,
"ConnectionResetError",
exception_runtime_error
);
define_exception!(TLS_ERROR, "TlsError", exception_runtime_error);

// HTTP protocol and request/response errors
define_exception!(REQUEST_ERROR, "RequestError", exception_runtime_error);
define_exception!(STATUS_ERROR, "StatusError", exception_runtime_error);
define_exception!(REDIRECT_ERROR, "RedirectError", exception_runtime_error);
define_exception!(TIMEOUT_ERROR, "TimeoutError", exception_runtime_error);

// Data processing and encoding errors
define_exception!(BODY_ERROR, "BodyError", exception_runtime_error);
define_exception!(DECODING_ERROR, "DecodingError", exception_runtime_error);

// Configuration and builder errors
define_exception!(BUILDER_ERROR, "BuilderError", exception_runtime_error);

/// Map [`wreq::Error`] to corresponding [`magnus::Error`]
pub fn wreq_error_to_magnus(ruby: &Ruby, err: wreq::Error) -> MagnusError {
let error_msg = err.to_string();
map_wreq_error!(
ruby,
err,
error_msg,
is_builder => BUILDER_ERROR,
is_body => BODY_ERROR,
is_tls => TLS_ERROR,
is_connection_reset => CONNECTION_RESET_ERROR,
is_connect => CONNECTION_ERROR,
is_decode => DECODING_ERROR,
is_redirect => REDIRECT_ERROR,
is_timeout => TIMEOUT_ERROR,
is_status => STATUS_ERROR,
is_request => REQUEST_ERROR,
)
}

pub fn include(ruby: &Ruby) {
Lazy::force(&CONNECTION_ERROR, ruby);
Lazy::force(&CONNECTION_RESET_ERROR, ruby);
Lazy::force(&TLS_ERROR, ruby);
Lazy::force(&REQUEST_ERROR, ruby);
Lazy::force(&STATUS_ERROR, ruby);
Lazy::force(&REDIRECT_ERROR, ruby);
Lazy::force(&TIMEOUT_ERROR, ruby);
Lazy::force(&BODY_ERROR, ruby);
Lazy::force(&DECODING_ERROR, ruby);
Lazy::force(&BUILDER_ERROR, ruby);
}
Comment on lines +72 to +83
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The include function doesn't return a Result, unlike http::include and client::include. While the current implementation uses unwrap() in the lazy initialization (which could panic), consider returning Result<(), Error> for consistency with other module includes and to allow proper error propagation if initialization fails.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

10 changes: 5 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@
#[macro_use]
mod macros;
mod client;
mod error;
mod http;
mod nogvl;

use std::sync::LazyLock;

use magnus::{Error, Ruby};
use magnus::{Error, RModule, Ruby, value::Lazy};

static RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
tokio::runtime::Builder::new_multi_thread()
Expand All @@ -34,15 +35,14 @@ static RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
.expect("Failed to initialize Tokio runtime")
});

fn format_magnus_error(ruby: &Ruby, err: wreq::Error) -> Error {
Error::new(ruby.exception_runtime_error(), err.to_string())
}
const RUBY_MODULE_NAME: &str = "Wreq";

/// wreq ruby binding
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let gem_module = ruby.define_module("Wreq")?;
let gem_module = ruby.define_module(RUBY_MODULE_NAME)?;
http::include(ruby, &gem_module)?;
client::include(ruby, &gem_module)?;
error::include(ruby);
Ok(())
}
7 changes: 0 additions & 7 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,6 @@ macro_rules! apply_option {
$builder = $builder.$method($transform(&value));
}
};
(set_if_some_map_ok, $builder:expr, $option:expr, $method:ident, $transform:expr) => {
if let Some(value) = $option.take() {
if let Ok(transformed) = $transform(value) {
$builder = $builder.$method(transformed);
}
}
};
(set_if_true, $builder:expr, $option:expr, $method:ident, $default:expr) => {
if $option.unwrap_or($default) {
$builder = $builder.$method();
Expand Down