use std::time::Duration;
use sentry_core::client_report::Reason as LossReason;
use sentry_core::TransportOptions;
use ureq::http::Response;
#[cfg(any(
feature = "rustls",
feature = "rustls-no-provider",
feature = "native-tls"
))]
use ureq::tls::{TlsConfig, TlsProvider};
use ureq::{Agent, Proxy};
use super::{
thread::{TransportThread, TransportThreadOptions},
RateLimiter, HTTP_PAYLOAD_TOO_LARGE, HTTP_PAYLOAD_TOO_LARGE_MESSAGE,
};
use crate::{sentry_debug, types::Scheme, ClientOptions, Envelope, Transport};
const HTTP_RATE_LIMIT_STATUS: u16 = 429;
#[cfg_attr(doc_cfg, doc(cfg(feature = "ureq")))]
pub struct UreqHttpTransport {
thread: TransportThread,
}
#[derive(Debug)]
#[must_use]
pub struct UreqHttpTransportOptions {
general_options: TransportOptions,
agent: Option<Agent>,
}
impl UreqHttpTransport {
#[inline]
#[deprecated = "use `UreqHttpTransportOptions::build` instead"]
pub fn new(options: &ClientOptions) -> Self {
let general_options = TransportOptions::try_from_client_options(options)
.expect("this method should only be called when options has a DSN");
UreqHttpTransportOptions::from(general_options).build()
}
#[inline]
#[deprecated = "use `UreqHttpTransportOptions::build` instead"]
pub fn with_agent(options: &ClientOptions, agent: Agent) -> Self {
let general_options = TransportOptions::try_from_client_options(options)
.expect("this method should only be called when options has a DSN");
UreqHttpTransportOptions::from(general_options)
.with_agent(agent)
.build()
}
#[inline]
pub(super) fn with_options(options: UreqHttpTransportOptions) -> Self {
let UreqHttpTransportOptions {
general_options:
TransportOptions {
dsn,
user_agent,
http_proxy,
https_proxy,
#[cfg(any(
feature = "native-tls",
feature = "rustls",
feature = "rustls-no-provider"
))]
accept_invalid_certs,
client_report_recorder,
..
},
agent,
} = options;
let scheme = dsn.scheme();
let agent = agent.unwrap_or_else(|| {
let mut builder = Agent::config_builder();
#[cfg(feature = "native-tls")]
{
builder = builder.tls_config(
TlsConfig::builder()
.provider(TlsProvider::NativeTls)
.disable_verification(accept_invalid_certs)
.build(),
);
}
#[cfg(any(feature = "rustls", feature = "rustls-no-provider"))]
{
builder = builder.tls_config(
TlsConfig::builder()
.provider(TlsProvider::Rustls)
.disable_verification(accept_invalid_certs)
.build(),
);
}
let mut maybe_proxy = None;
match (scheme, &http_proxy, &https_proxy) {
(Scheme::Https, _, Some(proxy)) => match Proxy::new(proxy.as_ref()) {
Ok(proxy) => {
maybe_proxy = Some(proxy);
}
Err(err) => {
sentry_debug!("invalid proxy: {:?}", err);
}
},
(_, Some(proxy), _) => match Proxy::new(proxy.as_ref()) {
Ok(proxy) => {
maybe_proxy = Some(proxy);
}
Err(err) => {
sentry_debug!("invalid proxy: {:?}", err);
}
},
_ => {}
}
builder = builder.proxy(maybe_proxy);
builder.build().new_agent()
});
let auth = dsn.to_auth(Some(&user_agent)).to_string();
let url = dsn.envelope_api_url().to_string();
let send_fn_client_report_recorder = client_report_recorder.clone();
let send_fn = move |envelope: Envelope, rl: &mut RateLimiter| {
let mut body = Vec::new();
envelope
.to_writer(&mut body)
.inspect_err(|_| {
send_fn_client_report_recorder
.record_lost_data(&envelope, LossReason::InternalError);
})
.expect("envelope should serialize successfully");
let request = agent
.post(&url)
.header("X-Sentry-Auth", &auth)
.config()
.http_status_as_error(false)
.build()
.send(&body);
match request {
Ok(mut response) => {
fn header_str<'a, B>(response: &'a Response<B>, key: &str) -> Option<&'a str> {
response.headers().get(key)?.to_str().ok()
}
if let Some(sentry_header) = header_str(&response, "x-sentry-rate-limits") {
rl.update_from_sentry_header(sentry_header);
} else if let Some(retry_after) = header_str(&response, "retry-after") {
rl.update_from_retry_after(retry_after);
} else if response.status() == HTTP_RATE_LIMIT_STATUS {
rl.update_from_429();
}
let response_status = response.status().as_u16();
match response.body_mut().read_to_string() {
Err(err) => {
sentry_debug!("Failed to read sentry response: {}", err);
}
Ok(text) => {
sentry_debug!("Get response: `{}`", text);
}
}
if response_status == HTTP_PAYLOAD_TOO_LARGE {
sentry_debug!("{HTTP_PAYLOAD_TOO_LARGE_MESSAGE}");
}
if (400..=599).contains(&response_status)
&& response_status != HTTP_RATE_LIMIT_STATUS
{
send_fn_client_report_recorder
.record_lost_data(&envelope, LossReason::SendError);
}
}
Err(err) => {
sentry_debug!("Failed to send envelope: {}", err);
send_fn_client_report_recorder
.record_lost_data(&envelope, LossReason::NetworkError);
}
}
};
let thread = TransportThreadOptions::new(send_fn)
.with_client_report_recorder(client_report_recorder)
.spawn_thread();
Self { thread }
}
}
impl Transport for UreqHttpTransport {
fn send_envelope(&self, envelope: Envelope) {
self.thread.send(envelope)
}
fn flush(&self, timeout: Duration) -> bool {
self.thread.flush(timeout)
}
fn shutdown(&self, timeout: Duration) -> bool {
self.flush(timeout)
}
}
impl From<TransportOptions> for UreqHttpTransportOptions {
#[inline]
fn from(value: TransportOptions) -> Self {
Self {
general_options: value,
agent: None,
}
}
}
impl UreqHttpTransportOptions {
#[inline]
pub fn with_agent(self, agent: Agent) -> Self {
let agent = Some(agent);
Self { agent, ..self }
}
#[inline]
pub fn build(self) -> UreqHttpTransport {
UreqHttpTransport::with_options(self)
}
}