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
37 changes: 30 additions & 7 deletions ext/wreq_rb/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use tokio_util::sync::CancellationToken;
use std::net::IpAddr;
use wreq::header::{HeaderMap, HeaderName, HeaderValue, OrigHeaderMap};
use wreq::tls::TlsVersion;
use wreq::IntoEmulation;
use wreq_util::{Emulation as BrowserEmulation, Platform as EmulationPlatform, Profile as BrowserProfile};

use crate::error::{generic_error, to_magnus_error, wreq_error};
Expand Down Expand Up @@ -235,13 +236,28 @@ fn build_emulation_option(
.build())
}

fn apply_request_emulation(
req: wreq::RequestBuilder,
option: BrowserEmulation,
header_order: Option<&OrigHeaderMap>,
) -> wreq::RequestBuilder {
let mut emulation = option.into_emulation();
if let Some(header_order) = header_order {
let mut merged_order = header_order.clone();
merged_order.extend(emulation.orig_headers);
emulation.orig_headers = merged_order;
}
req.emulation(emulation)
}

// --------------------------------------------------------------------------
// Ruby Client
// --------------------------------------------------------------------------

#[magnus::wrap(class = "Wreq::Client", free_immediately)]
struct Client {
inner: wreq::Client,
header_order: Option<OrigHeaderMap>,
cancel_token: std::sync::Mutex<CancellationToken>,
}

Expand All @@ -256,12 +272,14 @@ impl Client {

let mut builder = wreq::Client::builder()
.retry(wreq::retry::Policy::never());
let mut header_order = None;

if let Some(opts) = opts {
// Apply header_order BEFORE emulation so the user's ordering takes precedence
if let Some(ary) = hash_get_array(&opts, "header_order")? {
let orig = array_to_orig_header_map(ary)?;
builder = builder.orig_headers(orig);
builder = builder.orig_headers(orig.clone());
header_order = Some(orig);
}

if let Some(val) = hash_get_value(&opts, "emulation")? {
Expand Down Expand Up @@ -409,7 +427,11 @@ impl Client {
}

let client = builder.build().map_err(to_magnus_error)?;
Ok(Client { inner: client, cancel_token: std::sync::Mutex::new(CancellationToken::new()) })
Ok(Client {
inner: client,
header_order,
cancel_token: std::sync::Mutex::new(CancellationToken::new()),
})
}

/// client.get(url) or client.get(url, opts)
Expand Down Expand Up @@ -473,7 +495,7 @@ impl Client {
let mut req = self.inner.request(method, &url);

if let Some(opts) = opts {
req = apply_request_options(req, &opts)?;
req = apply_request_options(req, &opts, self.header_order.as_ref())?;
}

let client_token = self.cancel_token.lock().unwrap_or_else(|e| e.into_inner()).clone();
Expand Down Expand Up @@ -601,10 +623,10 @@ impl Client {

let mut req = self.inner.request(method, &url);
if let Some(shared) = shared {
req = apply_request_options(req, shared)?;
req = apply_request_options(req, shared, self.header_order.as_ref())?;
}
if let Some(item_opts) = item_opts {
req = apply_request_options(req, &item_opts)?;
req = apply_request_options(req, &item_opts, self.header_order.as_ref())?;
}
Ok(req)
}
Expand All @@ -621,19 +643,20 @@ fn value_to_method(val: Value) -> Result<wreq::Method, magnus::Error> {
fn apply_request_options(
mut req: wreq::RequestBuilder,
opts: &RHash,
header_order: Option<&OrigHeaderMap>,
) -> Result<wreq::RequestBuilder, magnus::Error> {
if let Some(val) = hash_get_value(opts, "emulation")? {
let ruby = unsafe { Ruby::get_unchecked() };
if val.is_kind_of(ruby.class_false_class()) {
// emulation: false — no per-request emulation override
} else if val.is_kind_of(ruby.class_true_class()) {
let opt = build_emulation_option(DEFAULT_EMULATION, opts)?;
req = req.emulation(opt);
req = apply_request_emulation(req, opt, header_order);
} else {
let name: String = TryConvert::try_convert(val)?;
let emu = parse_emulation(&name)?;
let opt = build_emulation_option(emu, opts)?;
req = req.emulation(opt);
req = apply_request_emulation(req, opt, header_order);
}
}

Expand Down
20 changes: 20 additions & 0 deletions test/client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,26 @@ def test_request_timeout_without_client_timeouts
assert_operator elapsed, :>=, 0.4
end

def test_header_order_takes_precedence_over_request_emulation
order = ["host", "accept", "user-agent"]
client = Wreq::Client.new(
emulation: false,
http1_only: true,
no_proxy: true,
header_order: order
)
received = capture_wire_headers do |url|
client.get(url, emulation: "chrome_145")
end

positions = order.map { |header| received.index(header) }.compact
assert_equal order.size, positions.size,
"Not all target headers found in: #{received.inspect}"
assert_equal positions.sort, positions,
"Expected client's header_order #{order.inspect} to take precedence " \
"over request emulation; got positions #{positions.inspect} in: #{received.inspect}"
end

private

def stalled_tls_request_duration(options, request_timeout: 3)
Expand Down