mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
fix(net): bounded request time, curl size and time caps, lock waits, sane prices
From the full-repo Codex audit: - MSN and Yahoo clients set connect and body timeouts but none for the response headers, so a server that stalled before answering hung the call; four such calls take every MCP call slot. Add a 30 s overall deadline. Every curl-impersonate run gets --max-time 600 and a 256 MiB --max-filesize, and output above that is rejected (curl cannot cap a response without a length); the Yahoo cookie fetch gets --max-time. - Ownership commands blocked on the database lock without limit, so a long sync or import, or a stuck process, held MCP call slots indefinitely. Wait up to 30 s, then fail with a "busy, try again" error. - A price of 1e20 saturated `as i64` and the change subtraction then overflowed. Prices that are not finite, negative, or above 1e12 IDR are treated as missing, and a quote's own price as an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
94aec74b09
commit
66207d24b6
6 changed files with 144 additions and 34 deletions
|
|
@ -30,6 +30,9 @@ impl YahooClient {
|
|||
let agent: ureq::Agent = ureq::Agent::config_builder()
|
||||
.timeout_connect(Some(Duration::from_secs(5)))
|
||||
.timeout_recv_body(Some(Duration::from_secs(10)))
|
||||
// A server that stalls before sending headers must not hold a
|
||||
// call (and an MCP call slot) forever.
|
||||
.timeout_global(Some(Duration::from_secs(30)))
|
||||
.build()
|
||||
.into();
|
||||
|
||||
|
|
@ -141,7 +144,14 @@ impl YahooClient {
|
|||
// Step 1: fetch fc.yahoo.com to set A3 cookie (returns 404 but writes cookie jar).
|
||||
// We allow non-zero exit here since 404 still writes the cookie.
|
||||
let _ = Command::new(binary)
|
||||
.args(["--silent", "--cookie-jar", cookie_jar_str, COOKIE_FETCH_URL])
|
||||
.args([
|
||||
"--silent",
|
||||
"--max-time",
|
||||
"30",
|
||||
"--cookie-jar",
|
||||
cookie_jar_str,
|
||||
COOKIE_FETCH_URL,
|
||||
])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.output();
|
||||
|
|
|
|||
|
|
@ -20,8 +20,12 @@ pub(super) fn parse_quote(symbol: &str, chart: &ChartResponse) -> Result<Quote,
|
|||
.ok_or_else(|| IdxError::SymbolNotFound(symbol.to_string()))?;
|
||||
let raw_prev_close = meta.previous_close.or(meta.chart_previous_close);
|
||||
|
||||
let price = round_price(raw_price);
|
||||
let prev_close = raw_prev_close.map(round_price);
|
||||
let price = round_price(raw_price).ok_or_else(|| {
|
||||
IdxError::ParseError(format!(
|
||||
"Yahoo sent an implausible price for {symbol}: {raw_price}"
|
||||
))
|
||||
})?;
|
||||
let prev_close = raw_prev_close.and_then(round_price);
|
||||
let change = prev_close.map(|p| price - p);
|
||||
let change_pct = raw_prev_close
|
||||
.filter(|p| *p != 0.0)
|
||||
|
|
@ -50,8 +54,8 @@ pub(super) fn parse_quote(symbol: &str, chart: &ChartResponse) -> Result<Quote,
|
|||
change_pct,
|
||||
volume: meta.regular_market_volume,
|
||||
market_cap: meta.market_cap,
|
||||
week52_high: meta.fifty_two_week_high.map(round_price),
|
||||
week52_low: meta.fifty_two_week_low.map(round_price),
|
||||
week52_high: meta.fifty_two_week_high.and_then(round_price),
|
||||
week52_low: meta.fifty_two_week_low.and_then(round_price),
|
||||
week52_position,
|
||||
range_signal,
|
||||
prev_close,
|
||||
|
|
@ -103,22 +107,22 @@ pub(super) fn parse_history(
|
|||
.open
|
||||
.as_ref()
|
||||
.and_then(|v| v.get(i).copied().flatten())
|
||||
.map(round_price);
|
||||
.and_then(round_price);
|
||||
let high = quote
|
||||
.high
|
||||
.as_ref()
|
||||
.and_then(|v| v.get(i).copied().flatten())
|
||||
.map(round_price);
|
||||
.and_then(round_price);
|
||||
let low = quote
|
||||
.low
|
||||
.as_ref()
|
||||
.and_then(|v| v.get(i).copied().flatten())
|
||||
.map(round_price);
|
||||
.and_then(round_price);
|
||||
let close = quote
|
||||
.close
|
||||
.as_ref()
|
||||
.and_then(|v| v.get(i).copied().flatten())
|
||||
.map(round_price);
|
||||
.and_then(round_price);
|
||||
let volume = quote
|
||||
.volume
|
||||
.as_ref()
|
||||
|
|
@ -202,8 +206,12 @@ pub(super) fn parse_fundamentals(
|
|||
})
|
||||
}
|
||||
|
||||
fn round_price(value: f64) -> i64 {
|
||||
value.round() as i64
|
||||
/// Prices are IDR; anything non-finite, negative, or above this is not a
|
||||
/// real quote, and treating it as missing keeps arithmetic from overflowing.
|
||||
const MAX_PRICE: f64 = 1e12;
|
||||
|
||||
fn round_price(value: f64) -> Option<i64> {
|
||||
(value.is_finite() && (0.0..=MAX_PRICE).contains(&value)).then(|| value.round() as i64)
|
||||
}
|
||||
|
||||
pub(super) fn map_yahoo_error(symbol: &str, endpoint: &str, err: &ChartError) -> IdxError {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue