Skip to content

feat(update):Add proxy option to update command#2281

Open
AccMoment wants to merge 3 commits into
Hmbown:mainfrom
AccMoment:add_proxy_option_to_update_command
Open

feat(update):Add proxy option to update command#2281
AccMoment wants to merge 3 commits into
Hmbown:mainfrom
AccMoment:add_proxy_option_to_update_command

Conversation

@AccMoment
Copy link
Copy Markdown

@AccMoment AccMoment commented May 27, 2026

Summary

Add proxy support to update command

Accept an optional --proxy argument in the self update command,
it's validate the proxy URL, test connectivity, and pass the proxy through all update HTTP
requests made during the update workflow.

Testing

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features
  • cargo test --workspace --all-features

Checklist

  • Updated docs or comments as needed
  • Added or updated tests where relevant
  • Verified TUI behavior manually if UI changes

Greptile Summary

This PR adds an optional --proxy argument to the codewhale update command, routing all update-related HTTP requests (release metadata fetches and binary downloads) through the specified proxy. It also enables the socks reqwest feature so SOCKS5 proxies work correctly at runtime.

  • validate_and_build_proxy parses and validates the proxy URL, then threads the resulting reqwest::Proxy through update_http_client, fetch_latest_release, and download_url — the connectivity pre-check from an earlier iteration was correctly removed.
  • Cargo.toml adds the "socks" feature to the workspace reqwest dependency, closing the gap between the advertised SOCKS5 support and what reqwest could actually deliver.
  • New tests in lib.rs cover CLI parsing with and without --proxy, as well as valid/invalid inputs to validate_and_build_proxy.

Confidence Score: 5/5

Safe to merge — proxy option is purely additive, the opt-in feature flag is correct, and the HTTP client is properly threaded throughout the update workflow.

The change is narrowly scoped: a new optional CLI flag that, when absent, leaves all existing behaviour unchanged. The socks feature gap is closed, the problematic connectivity pre-check from earlier review rounds has been removed, and all three HTTP paths (release metadata, checksum manifest, binary download) correctly receive the proxy. No correctness regressions identified.

No files require special attention.

Important Files Changed

Filename Overview
crates/cli/src/update.rs Adds proxy propagation through all update HTTP calls; validates URL format and builds a reqwest::Proxy. Connectivity pre-check was correctly removed. Stale doc comment still mentions connectivity testing.
crates/cli/src/lib.rs Adds proxy: Option to UpdateArgs with doc comment and wires it through to run_update. New tests cover both the happy path and validation; one test function name is misspelled (udpate_parse_without_proxy) but that was flagged in prior review rounds.
Cargo.toml Adds the socks feature to the workspace reqwest dependency, enabling SOCKS5 proxy support that was previously advertised but not compiled in.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as codewhale CLI (lib.rs)
    participant Update as run_update (update.rs)
    participant Proxy as validate_and_build_proxy
    participant HTTP as update_http_client
    participant GitHub as GitHub API / CDN

    User->>CLI: codewhale update --proxy socks5://host:1080
    CLI->>Update: run_update(beta, Some("socks5://host:1080"))
    Update->>Proxy: validate_and_build_proxy("socks5://host:1080")
    Proxy-->>Update: Ok(reqwest::Proxy)
    Update->>HTTP: "update_http_client(&Some(proxy))"
    HTTP-->>Update: blocking::Client (proxy configured)
    Update->>GitHub: GET /repos/.../releases/latest
    GitHub-->>Update: Release JSON
    Update->>HTTP: "update_http_client(&Some(proxy))"
    HTTP-->>Update: blocking::Client
    Update->>GitHub: GET checksum manifest URL
    GitHub-->>Update: SHA256 bytes
    Update->>HTTP: "update_http_client(&Some(proxy))"
    HTTP-->>Update: blocking::Client
    Update->>GitHub: GET binary asset URL
    GitHub-->>Update: binary bytes
    Update-->>User: Updated successfully
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (3): Last reviewed commit: "enable reqwest socks features" | Re-trigger Greptile

Update docs to introduce update command proxy options
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for a --proxy argument in the codewhale update command, allowing users to perform self-updates through a proxy. It updates the documentation across multiple languages, adds CLI argument parsing, integrates the proxy configuration into the HTTP client builder, and includes corresponding unit tests. The review feedback highlights critical issues with the proxy implementation: the blocking connectivity test against GitHub's API should be removed to prevent failures when using custom mirrors, avoid wasting API rate limits, and reduce latency. Additionally, typos in the test names (udpate instead of update) and an invalid URL format in the tests should be corrected.

Comment thread crates/cli/src/update.rs Outdated
Comment on lines +39 to +43
let proxy = if let Some(proxy_str) = &args.proxy {
validate_and_build_proxy(proxy_str)?
} else {
None
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The proxy connectivity test in validate_and_build_proxy is performed against LATEST_RELEASE_URL (GitHub API). However, if a user is using a mirror (e.g., via CODEWHALE_USE_CNB_MIRROR or CODEWHALE_RELEASE_BASE_URL because GitHub is blocked or slow), this connectivity test will fail and prevent the update from proceeding, even if the mirror is perfectly reachable through the proxy.

Additionally, this extra blocking request consumes an unnecessary GitHub API rate limit point and adds latency to every update command.

We should simplify validate_and_build_proxy to only parse and build the proxy without performing a blocking connectivity test.

    let proxy = if let Some(proxy_str) = &args.proxy {
        Some(validate_and_build_proxy(proxy_str)?)
    } else {
        None
    };

Comment thread crates/cli/src/update.rs Outdated
Comment on lines +188 to +228
fn validate_and_build_proxy(proxy_str: &str) -> Result<Option<Proxy>> {
let valid_url = reqwest::Url::parse(proxy_str).with_context(|| {
format!(
"invalid proxy URL: {proxy_str}\n\
Expected format: http://host:port, https://host:port, or socks5://host:port"
)
})?;

let proxy = reqwest::Proxy::all(valid_url)?;

// Quick connectivity test through the proxy
let client = reqwest::blocking::Client::builder()
.proxy(proxy.clone())
.user_agent(UPDATE_USER_AGENT)
.timeout(Duration::from_secs(10))
.build()
.context("Could not build proxy HTTP client")?;

match client.head(LATEST_RELEASE_URL).send() {
Ok(_) => Ok(Some(proxy)),
Err(e) => {
// Give a clear actionable error rather than a raw reqwest error
let hint = if e.is_timeout() || e.is_connect() {
"could not connect to the proxy server"
} else if e.is_request() {
"the request was sent but no response was received"
} else {
"an unexpected network error occurred"
};
bail!(
"proxy connectivity failed: {hint}\n\
Proxy URL: {proxy_str}\n\
Details: {e}\n\
Please verify:\n\
- The proxy URL is correct\n\
- The proxy server is running and reachable\n\
- The proxy allows outbound connections to api.github.com"
)
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Simplify validate_and_build_proxy by removing the blocking connectivity test. This avoids:

  1. Breaking updates when using a mirror in environments where GitHub is blocked.
  2. Wasting GitHub API rate limits on an extra round-trip request.
  3. Unnecessary latency during the update process.
fn validate_and_build_proxy(proxy_str: &str) -> Result<Proxy> {
    let valid_url = reqwest::Url::parse(proxy_str).with_context(|| {
        format!("invalid proxy URL: {proxy_str}. Expected format: http://host:port, https://host:port, or socks5://host:port")
    })?;

    let proxy = reqwest::Proxy::all(valid_url)?;
    Ok(proxy)
}

Comment thread crates/cli/src/lib.rs
Comment on lines +2438 to +2450
#[test]
fn udpate_parse_with_proxy() {
let cli = parse_ok(&["deepseek", "update", "--proxy", "http:localhost:7897"]);

let args = match cli.command {
Some(Commands::Update(args)) => args,
other => panic!("expected Update with proxy, got {other:?}"),
};
assert_eq!(
args.proxy.expect("should have proxy"),
"http:localhost:7897"
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Fix the typo in the test name (udpate -> update) and use a valid URL format (http://localhost:7897 instead of http:localhost:7897) to ensure standard URL parsing behavior.

Suggested change
#[test]
fn udpate_parse_with_proxy() {
let cli = parse_ok(&["deepseek", "update", "--proxy", "http:localhost:7897"]);
let args = match cli.command {
Some(Commands::Update(args)) => args,
other => panic!("expected Update with proxy, got {other:?}"),
};
assert_eq!(
args.proxy.expect("should have proxy"),
"http:localhost:7897"
);
}
#[test]
fn update_parse_with_proxy() {
let cli = parse_ok(&["deepseek", "update", "--proxy", "http://localhost:7897"]);
let args = match cli.command {
Some(Commands::Update(args)) => args,
other => panic!("expected Update with proxy, got {other:?}"),
};
assert_eq!(
args.proxy.expect("should have proxy"),
"http://localhost:7897"
);
}

Comment thread crates/cli/src/lib.rs
Comment on lines +2452 to +2461
#[test]
fn udpate_parse_without_proxy() {
let cli = parse_ok(&["deepseek", "update"]);

let args = match cli.command {
Some(Commands::Update(args)) => args,
other => panic!("expected Update, got {other:?}"),
};
assert!(args.proxy.is_none());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Fix the typo in the test name (udpate -> update).

Suggested change
#[test]
fn udpate_parse_without_proxy() {
let cli = parse_ok(&["deepseek", "update"]);
let args = match cli.command {
Some(Commands::Update(args)) => args,
other => panic!("expected Update, got {other:?}"),
};
assert!(args.proxy.is_none());
}
#[test]
fn update_parse_without_proxy() {
let cli = parse_ok(&["deepseek", "update"]);
let args = match cli.command {
Some(Commands::Update(args)) => args,
other => panic!("expected Update, got {other:?}"),
};
assert!(args.proxy.is_none());
}

Comment thread crates/cli/src/update.rs Outdated
Comment thread crates/cli/src/update.rs Outdated
Comment thread crates/cli/src/lib.rs Outdated
Comment thread crates/cli/src/lib.rs
Comment thread crates/cli/src/lib.rs
Comment thread crates/cli/src/lib.rs
@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented May 27, 2026

Independent review: merged into a fresh worktree on top of origin/main; cargo check -p codewhale-cli and the two new udpate_parse_* tests pass cleanly.

Findings greptile did not raise:

  1. validate_and_build_proxy is mirror-mode unaware (crates/cli/src/update.rs:188-228). When CODEWHALE_RELEASE_BASE_URL, CODEWHALE_USE_CNB_MIRROR, or the legacy equivalents are set, fetch_latest_release skips GitHub entirely (update.rs:413-423) — but the eager HEAD still targets LATEST_RELEASE_URL. A user behind a corporate proxy that only routes to a CNB mirror will be hard-blocked by a precheck for an endpoint the real flow never hits. The check should either be skipped when release_base_url_from_env(&version).is_some() or aim at the mirror URL.

  2. validate_and_build_proxy itself has zero test coverage. The only new tests (crates/cli/src/lib.rs:2438,2452) exercise clap parsing; the URL-validation, reqwest::Proxy::all, and error-branch logic where the actual bugs live are unverified. A unit test against a localhost tiny_http (the pattern already used by serve_http_once in this file) would cover both the success and proxy-down paths.

  3. Minor coupling smell: use crate::UpdateArgs in update.rs:7 reverses the dependency direction (cli was the consumer of update). Passing beta: bool, proxy: Option<&str> keeps run_update decoupled from the clap-derived struct shape.

v0.8.48 (#2256) compatibility: clean (both touch crates/cli/src/lib.rs but in disjoint regions; sequential merges apply without conflict).

reimplement validate_and_build_proxy and add test
Comment thread crates/cli/src/update.rs
Comment on lines +184 to +195
/// Validate the proxy URL and optionally test connectivity before proceeding.
pub(crate) fn validate_and_build_proxy(proxy_str: &str) -> Result<Proxy> {
let valid_url = reqwest::Url::parse(proxy_str).with_context(|| {
format!(
"invalid proxy URL: {proxy_str}\n\
Expected format: http://host:port, https://host:port, or socks5://host:port"
)
})?;

let proxy = reqwest::Proxy::all(valid_url)?;
Ok(proxy)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 SOCKS5 proxy advertised but feature not enabled

The doc comment on UpdateArgs::proxy and the error message inside validate_and_build_proxy both list socks5://host:port as a supported format. However, the reqwest workspace dependency is declared with default-features = false and only ["json", "rustls", "blocking"] features are enabled — the "socks" feature that gates SOCKS5 tunnel support is absent. Proxy::all("socks5://...") will return Ok at validation time (the URI is stored without scheme validation), but when a request is made through the proxy, reqwest has no SOCKS5 connector and will fail with a confusing runtime error. Either add "socks" to the reqwest features list in crates/cli/Cargo.toml (and the workspace), or remove socks5://host:port from all user-facing hints so users aren't misled.

Fix in Codex Fix in Claude Code Fix in Cursor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants