Exchange rate + update dependencies#68
Conversation
|
I now have a complete picture of the PR. Here's my review: PR #68 review — Exchange rate endpoint + dependency bumpsGreat little feature, PR #68 cleanly adds a 1. Security: API key can leak through error messages [must fix]The provider puts the API key in the URL path ( fn map_exchange_rate_error(err: ExchangeRateError) -> (StatusCode, String) {
match err {
ExchangeRateError::Api(err) => (StatusCode::BAD_REQUEST, err),
ExchangeRateError::Http(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
ExchangeRateError::Json(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
}
}This can leak the key to any external caller who provokes a network error. Also, fn map_exchange_rate_error(err: ExchangeRateError) -> (StatusCode, String) {
tracing::error!("Exchange rate error: {}", err);
match err {
ExchangeRateError::Api(_) => (StatusCode::BAD_GATEWAY,
"Exchange rate provider returned an error".to_string()),
ExchangeRateError::Http(_) => (StatusCode::BAD_GATEWAY,
"Failed to fetch exchange rates".to_string()),
ExchangeRateError::Json(_) => (StatusCode::BAD_GATEWAY,
"Failed to parse exchange rate response".to_string()),
}
}Adding a Display sanitiser ( 2. Silent fallbacks violate the "fail early" rule [must fix]Three spots in let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap_or_else(|_| reqwest::Client::new()); // (A) loses the timeouttime_next_update_unix: i64::try_from(time_next).unwrap_or(i64::MAX), // (B) cache "never expires"fn now_unix_seconds() -> i64 {
SystemTime::now().duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0) // (C) cache always stale
}
For (B) and (C), at minimum log a warning; ideally surface them as 3. DRY:
|
|
What is this word list change all about? Do we need this? Also why is hodl and nft back in this list, that shouldn't be in there... |
https://github.com/Quantus-Network/qp-human-checkphrase/blob/main/final_wordlist.txt well it's there, in main branch |
- checkphrase wordlist - error leaking key - etc
|
The PR head hasn't moved since yesterday — still Wordlist: now in sync with upstreamThe PR's (Both still carry Code review: status unchanged since yesterdayThe branch hasn't been updated, so the same picture as my prior re-review applies: Still must-fix before merge1. Silent failure in The inner error is dropped on the floor — no tracing::error!("Exchange rate error: {}", err);2. DRY: Simplest fix: Still worth flagging (not blocking)
What I'd merge todayOnce #1 lands the PR is mergeable in my book. #2 is a 30-second polish — would do it at the same time. Everything else (4, 5, 6, 8 from the original review) was follow-up material. Want me to push those two fixes onto Related context: Initial PR 68 review and yesterday's re-review. |
- map_exchange_rate_error now logs each variant via tracing::error! before returning a generic client message, so failures are no longer silent. Http variant is sanitised with reqwest::Error::without_url to avoid leaking the api_key in the URL. - new_test now reuses new() to avoid duplicating the client/cache construction.
PR #68 — Updated review after
|
| # | Issue | Fix |
|---|---|---|
| 1 | Silent failure in map_exchange_rate_error |
Each variant now logs via tracing::error! before returning a generic client message |
| 1b | Potential API key leak in Http variant |
reqwest::Error::without_url() strips the URL (which contains the key) before logging |
| 2 | DRY: new_test duplicated new's client construction |
new_test now delegates to Self::new(\"test-key\") and overwrites base_url |
fn map_exchange_rate_error(err: ExchangeRateError) -> (StatusCode, String) {
match err {
ExchangeRateError::Api(detail) => {
tracing::error!(\"Exchange rate API error: {}\", detail);
(StatusCode::BAD_GATEWAY, \"Failed to fetch exchange rates\".to_string())
}
ExchangeRateError::Http(e) => {
tracing::error!(\"Exchange rate HTTP error: {}\", e.without_url());
(StatusCode::BAD_GATEWAY, \"Failed to fetch exchange rates\".to_string())
}
// ...
}
}#[cfg(test)]
impl ExchangeRateService {
fn new_test(base_url: String) -> Self {
let mut service = Self::new(\"test-key\");
service.base_url = base_url;
service
}
}Status of the rest of the original review
| # | Topic | Status |
|---|---|---|
| 1 | API key leak via error messages | Fixed in e1d2b9c, hardened further in de9397a |
| 2 | Silent fallbacks (unwrap_or_else, unwrap_or(i64::MAX), unwrap_or(0)) |
Fixed in e1d2b9c |
| 3 | DRY in new_test |
Fixed in de9397a |
| 4 | Cache mutex held across HTTP call | Fixed in e1d2b9c (Arc<RwLock<Option<_>>>, guard scoped before .await) |
| 5 | Handler dropped freshness signal | Fixed in e1d2b9c (returns full ExchangeRateSnapshot) |
| 6 | HashMap-keyed cache YAGNI |
Fixed in e1d2b9c (collapsed to Option<_>) |
| 7 | Wordlist concerns (hodl/nft) |
Fixed in e1d2b9c; verified byte-identical to upstream qp-human-checkphrase@main |
| 8 | Router-level smoke test | Not done — fine as a follow-up |
| 9a | time_last_update_unix #[allow(dead_code)] |
Not done — trivial, leave or follow-up |
| 9b | result: String could be enum |
Not done — non-blocking |
Verdict
LGTM, merging now. Outstanding items (router smoke test, dead-code field) are non-blocking and can be picked up in a follow-up. Nice work iterating on this.
Summary