Skip to content

Make --network take precedence over rpc-url/passphrase env vars#2579

Open
fnando wants to merge 2 commits into
mainfrom
network-flag-precedence
Open

Make --network take precedence over rpc-url/passphrase env vars#2579
fnando wants to merge 2 commits into
mainfrom
network-flag-precedence

Conversation

@fnando
Copy link
Copy Markdown
Member

@fnando fnando commented May 14, 2026

What

Close stellar/stellar-cli-internal#85.

Two changes to network resolution in Args::get():

  • A named network (--network NAME, STELLAR_NETWORK, or stellar network use NAME) now always resolves via the on-disk config and ignores any rpc-url / network-passphrase set in any source. Previously, when all three were set, the wildcard match arm silently picked the rpc tuple and discarded the named network.
  • When the named network is selected with rpc-url or network-passphrase also set from any source, a one-shot warning is printed showing both the flag and env-var names of the values being ignored. Honors --quiet.
  • New CONFIGURATION section in stellar --help documenting the precedence (flag > env > use) and the named-network rule.

Internal: removes the dead Error::CannotUseBothRpcAndNetwork variant (never returned anywhere). Adds a tiny print::set_quiet/is_quiet OnceLock pair so Args::get can honor the global --quiet flag without threading it through 25 call sites.

Why

If a .env file set STELLAR_RPC_URL + STELLAR_NETWORK_PASSPHRASE, then running stellar … --network mainnet would silently send the request to whatever rpc the env happened to specify, ignoring the explicit --network flag. A named network is the user's clearer intent; resolving it via the on-disk config is what they meant.

The warning and help-text additions surface the precedence rule so users don't get surprised again — e.g. a developer with STELLAR_RPC_URL set in their shell who then runs stellar network info --network mainnet will see exactly what's happening.

Known limitations

N/A

Copilot AI review requested due to automatic review settings May 14, 2026 01:27
@github-project-automation github-project-automation Bot moved this to Backlog (Not Ready) in DevX May 14, 2026
@claude
Copy link
Copy Markdown

claude Bot commented May 14, 2026

Claude finished @fnando's task in 4m 14s —— View job


Claude Code Review

  • Read changed files
  • Analyze network resolution logic
  • Check for bugs, security, and performance issues
  • Post review feedback (4 inline comments + summary below)

Summary

Overall this is a solid bug fix with thoughtful UX (the warning + help-text update is exactly the right mitigation for a precedence change). The implementation is small, well-commented, and the integration tests cover the warning matrix nicely. A few concerns worth discussing before merge — none are blockers, but several deserve a decision on intent.

Behavior changes worth flagging in the changelog

  1. --network NAME --rpc-url X --network-passphrase Y reverses precedence.
    Old: rpc tuple won, named network ignored. New: named network wins, rpc tuple ignored (with a warning). This is the intended fix, but it's a SemVer-relevant behavior change — users who deliberately combined all three on the CLI to override a network's rpc will now silently get the on-disk rpc.

  2. Stale STELLAR_NETWORK=foo in .env is now an error. (inline comment)
    Previously, a stale STELLAR_NETWORK would be ignored if a working rpc tuple was provided. Now read_network is called eagerly and errors with ConfigMissing if the named network isn't in DEFAULTS and isn't on disk. Decide whether to fall through to the tuple on ConfigMissing, or accept the strict behavior and document it.

  3. Documented precedence (CLI > env > use) is contradicted by the "extra rule" when the user explicitly types --rpc-url.
    set_env_from_config writes STELLAR_NETWORK from stellar network use NAME into the environment. Clap then loads it as if it were typed. So after stellar network use testnet, running stellar tx ... --rpc-url=X --network-passphrase=Y silently ignores the explicit CLI flags. The warning helps, but the help text says CLI > env > use. To honor the documented precedence, you'd need to use ArgMatches::value_source() and ignore rpc/passphrase only when their source ranks lower than the network's source.

Implementation notes

  • OnceLock<bool> for quiet is pragmatic, but it's process-global state. Worth a doc-comment warning that tests should not call set_quiet directly (cross-test pollution in cargo test).
  • Warning wording says --network=local takes precedence even when the user typed no --network flag (env-only and use-only cases). Consider source-agnostic phrasing like network "local" takes precedence, or surface the source (from STELLAR_NETWORK).
  • --rpc-url / STELLAR_RPC_URL scans as one combined token; or is unambiguous.
  • Format allocation in warn_if_overridden happens even when --quiet; a short-circuit at the top of call_once avoids it.
  • Once is process-global, which is fine for CLI but means a second Args::get with overrides won't warn even if the first didn't. Just a future-use caveat.

Tests

  • The unit test network_name_wins_over_rpc_tuple uses "testnet", which lives in DEFAULTS — so the tmpdir set via STELLAR_CONFIG_HOME is never actually read. The test proves "built-in default beats rpc tuple", not "on-disk user-saved network beats rpc tuple". Adding a custom-on-disk-network variant would close that gap.
  • The warning emission is only covered by integration tests; capturing it at the unit level (e.g. via stderr buffer) would make the fast-loop catch regressions earlier.

Things that look good

  • Removing the dead CannotUseBothRpcAndNetwork variant — nice cleanup.
  • The Once guard against duplicate warnings in multi-get flows (sign + sign_fee_bump) is the right call.
  • The CONFIGURATION section in stellar --help and FULL_HELP_DOCS.md is well-written and exactly what users need.
  • Warning goes to stderr so it doesn't pollute JSON stdout — good.
  • --quiet is honored, and there's an integration test for it.

See inline comments on cmd/soroban-cli/src/config/network.rs and cmd/soroban-cli/src/print.rs for specifics.
Branch: network-flag-precedence

@fnando fnando moved this from Backlog (Not Ready) to Needs Review in DevX May 14, 2026
@fnando fnando self-assigned this May 14, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates network argument resolution so a named network takes precedence over RPC URL/passphrase settings, adds a one-time warning for ignored RPC settings, and documents the precedence rules in CLI help.

Changes:

  • Prioritizes named network resolution in Args::get().
  • Adds quiet-aware warning plumbing and integration/unit coverage for precedence behavior.
  • Updates full help documentation with configuration precedence details.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
FULL_HELP_DOCS.md Adds generated/help documentation for configuration precedence.
cmd/soroban-cli/src/print.rs Adds global quiet-state helpers for deep warning emission.
cmd/soroban-cli/src/config/network.rs Changes network resolution precedence, adds warning logic, removes dead error variant, and adds a unit test.
cmd/soroban-cli/src/commands/mod.rs Adds CONFIGURATION section to CLI long help.
cmd/soroban-cli/src/cli.rs Records parsed --quiet state after CLI parsing.
cmd/crates/soroban-test/tests/it/integration/network.rs Adds integration tests for warning behavior and quiet suppression.

Comment on lines +756 to +757

#[tokio::test]
Comment on lines +131 to +134
(Some(network), rpc_url, network_passphrase) => {
warn_if_overridden(network, rpc_url.as_deref(), network_passphrase.as_deref());
Ok(locator.read_network(network)?)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Precedence violation when --rpc-url is an explicit CLI flag and --network comes from a use default.

set_env_from_config in cli.rs populates STELLAR_NETWORK from stellar network use NAME. Clap then assigns it to self.network via env = "STELLAR_NETWORK". By the time we reach this arm, self.network = Some("…") is indistinguishable from a CLI-typed --network flag.

So a user who runs:

stellar network use testnet           # sets the on-disk default
stellar tx ... --rpc-url=http://custom.example.com --network-passphrase=foo

will have their explicit --rpc-url CLI flag silently overridden by the use default — a (use, CLI, CLI) source-tuple. That contradicts the precedence ladder you just added to the help text (CLI > env > use). The new "extra rule" documents the carve-out, but the more typical user expectation is that a flag they typed wins over a config default they may have forgotten about.

If you want to honor the documented precedence rigorously, clap exposes ArgMatches::value_source() which lets you check whether each value came from CommandLine, EnvVariable, or DefaultValue/programmatic. The rule could then be: ignore rpc-url/passphrase only when they have a strictly lower source than the network name. That's more code but it matches the documented precedence and avoids surprising the network use user.

At minimum, this trade-off (network always wins, regardless of source) deserves to be called out explicitly in CHANGELOG / release notes since it's a behavior change.

Comment on lines +99 to +117
// Emits the network-precedence warning at most once per process so commands
// that call `Args::get` multiple times (e.g. sign + sign_fee_bump) don't spam
// the user. Honors the global `--quiet` flag recorded in `cli::main`.
fn warn_if_overridden(network: &str, rpc_url: Option<&str>, network_passphrase: Option<&str>) {
static WARN_ONCE: Once = Once::new();
let ignored = match (rpc_url.is_some(), network_passphrase.is_some()) {
(true, true) => {
"--rpc-url / STELLAR_RPC_URL and --network-passphrase / STELLAR_NETWORK_PASSPHRASE"
}
(true, false) => "--rpc-url / STELLAR_RPC_URL",
(false, true) => "--network-passphrase / STELLAR_NETWORK_PASSPHRASE",
(false, false) => return,
};
WARN_ONCE.call_once(|| {
Print::new(print::is_quiet()).warnln(format!(
"--network={network} takes precedence; ignoring {ignored}"
));
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A few thoughts on this helper:

1. The --network={network} wording is misleading when the source is env / use.
The third integration test (network_flag_warning_fires_for_env_only_network) intentionally exercises a setup where the user typed no --network flag — only STELLAR_NETWORK=local. The user then sees --network=local takes precedence and may legitimately wonder which --network flag the warning refers to. Consider phrasing the warning in a source-agnostic way, e.g. network "local" takes precedence (or surfacing the actual source: network "local" (from STELLAR_NETWORK) takes precedence). This is also where STELLAR_NETWORK_SOURCE, already set by set_env_value_from_config, could be consulted to disambiguate env vs use.

2. --rpc-url / STELLAR_RPC_URL reads as one combined thing.
Slash-joined names can scan as "the rpc-url/stellar-rpc-url option". Minor, but or is unambiguous: ignoring --rpc-url or STELLAR_RPC_URL.

3. The Once is process-global.
That's fine for the typical CLI invocation (one process, one warning). It's worth knowing that in a long-lived test/library context, a second Args::get with overrides would be silenced even if the first call had none — Once doesn't reset. Not a bug given current usage, just a caveat for future use.

4. The Print::new(...) allocation only matters when not quiet.
You can short-circuit before formatting the message: if print::is_quiet() { return; } at the top of the call_once. Saves a format! allocation when --quiet is set. Trivial.

Comment on lines +11 to +24
static QUIET: OnceLock<bool> = OnceLock::new();

/// Record whether `--quiet` was passed on the command line. Called once from
/// `cli::main` after parsing so resolvers running deep in the call stack can
/// honor the global flag without it being threaded through every signature.
pub fn set_quiet(quiet: bool) {
let _ = QUIET.set(quiet);
}

/// Read the recorded `--quiet` flag, defaulting to `false` if [`set_quiet`]
/// hasn't run yet (e.g. in unit tests that don't go through `cli::main`).
pub fn is_quiet() -> bool {
*QUIET.get().unwrap_or(&false)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A process-global OnceLock<bool> is a pragmatic fix for not threading quiet through 25 call sites, but it's worth flagging a couple of properties:

  • Cross-test pollution risk. If any future test calls set_quiet(true) directly (or via a code path that does), every subsequent test in the same cargo test process sees is_quiet() == true. The current PR is safe because all integration tests use subprocesses (new_assert_cmd), but a future in-process unit test that touches this could silently mute warnings in unrelated tests. A short doc-comment warning against calling set_quiet from tests would be helpful.

  • let _ = QUIET.set(quiet); silently swallows the second-set error. That's fine because cli::main only calls it once, but it would be more robust to either debug_assert!(QUIET.set(quiet).is_ok()) or rename it to init_quiet to signal "call exactly once".

  • Mild alternative if you wanted to avoid global state entirely: thread an Arc<Print> (or just the bool) through Args::get via the locator: &locator::Args parameter — the locator already carries config-dir state through the call graph. Not necessarily worth the refactor, but it would localize the side effect.

Comment on lines +756 to +774

#[tokio::test]
async fn network_name_wins_over_rpc_tuple() {
use crate::test_utils::with_env_set;

let tmp = tempfile::tempdir().unwrap();
with_env_set("STELLAR_CONFIG_HOME", tmp.path(), || {
let args = Args {
network: Some("testnet".to_string()),
rpc_url: Some("http://other.example.com:59999".to_string()),
network_passphrase: Some("Some Other Passphrase".to_string()),
rpc_headers: Vec::new(),
};

let result = args.get(&locator::Args::default()).unwrap();

assert_eq!(result.rpc_url, "https://soroban-testnet.stellar.org");
assert_eq!(result.network_passphrase, passphrase::TESTNET);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This test is a good smoke test of the precedence flip, but note that "testnet" is hard-coded in DEFAULTS (see locator.rs:390), so read_network returns the built-in default without ever touching the tmpdir you set up via STELLAR_CONFIG_HOME. In effect, this only proves that a built-in default beats the rpc tuple — not that a user-saved (stellar network add) on-disk network does.

To exercise the actual on-disk path (the case from the PR description: ".env file sets STELLAR_RPC_URL + STELLAR_NETWORK_PASSPHRASE, user has saved a custom network"), you'd need to either:

  • write a network JSON under tmp/network/foo.json and use network: Some("foo"), or
  • assert on a named network that overrides the built-in (e.g. testnet with a custom on-disk rpc).

Also worth a unit-level assertion that warn_if_overridden did/didn't fire, even if just by capturing stderr — currently that's only covered by the integration tests, which means the warning is silently untested in the fast unit-test loop.

@leighmcculloch
Copy link
Copy Markdown
Member

When the named network is selected with rpc-url or network-passphrase also set from any source, a one-shot warning is printed showing both the flag and env-var names of the values being ignored.

This is interesting and conflicts with my own workflows and so I suspect others workflows too.

I have selected a network by name to get the passphrase and then overridden the rpc url. That seems like reasonable behaviour too.

any source

I think named network flag precedes all network env vars is intuitive, but the part that isn't is doing --network mainnet --rpc-url mypaidinstance and the last flag being ignored. Am I understanding correctly that is also part of the new behavior?

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

Labels

None yet

Projects

Status: Needs Review

Development

Successfully merging this pull request may close these issues.

3 participants