Description
I encountered an unexpected behavior when using pip download using uv run --no-project --with python -m pip. There was a strange behavior where I find out somehow --no-input affect the result of auth.
Contexet
Download/install a package from a private index that answers with a 302 redirect whose Location carries embedded Basic-auth credentials (https://user:token@upstream/...) — a common setup with devpi in front of Artifactory/JFrog, and similar proxy configurations — while passing --no-input (e.g. in CI).
What happened
The download fails with 401 Unauthorized, but only when --no-input is used. The exact same command without --no-input succeeds.
Root cause
When an index issues a cross-origin 302, the vendored requests session strips the Authorization header (should_strip_auth), so the upstream host receives no credentials and returns 401. pip's handle_401 (src/pip/_internal/network/auth.py) is supposed to recover: the embedded credentials are still present in resp.url, and _get_new_credentials() extracts URL-embedded credentials before it ever touches keyring or prompts.
Expected behavior
--no-input shouldn't affect authentication and expect same behavior.
pip version
25.3
Python version
3.13
OS
MacOS/Linux
How to Reproduce
When an index issues a cross-origin 302, the vendored requests session strips the Authorization header (should_strip_auth), so the upstream host receives no credentials and returns 401. pip's handle_401 (src/pip/_internal/network/auth.py) is supposed to recover: the embedded credentials are still present in resp.url, and _get_new_credentials() extracts URL-embedded credentials before it ever touches keyring or prompts.
The problem is that this recovery call is gated behind use_keyring:
# src/pip/_internal/network/auth.py, handle_401()
username, password = None, None
# Query the keyring for credentials:
if self.use_keyring: # <-- gate
username, password = self._get_new_credentials(
resp.url,
allow_netrc=False,
allow_keyring=True,
)
if not self.prompting and not username and not password:
return resp # <-- returns the 401 unretriedreturns the 401.
--no-input is documented as "Disable prompting for input", but it also silently disables this non-interactive redirect-credential recovery.
Minimal reproduction
This example is generated with AI, but I did test it to confirm the behavior is same as what I experience.
Self-contained, stdlib + pip only (no external index needed). Two local servers stand in for "index → upstream"; only auth.prompting (what --no-input sets) differs between the two attempts:
import base64, threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from pip._internal.network.auth import MultiDomainBasicAuth
from pip._vendor import requests
USER, TOKEN = "user", "token"
EXPECTED = "Basic " + base64.b64encode(f"{USER}:{TOKEN}".encode()).decode()
class Upstream(BaseHTTPRequestHandler):
def do_GET(self):
if self.headers.get("Authorization") == EXPECTED:
self.send_response(200); self.end_headers(); self.wfile.write(b"OK")
else:
self.send_response(401)
self.send_header("WWW-Authenticate", 'Basic realm="x"'); self.end_headers()
def log_message(self, *a): pass
def index(location):
class Index(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(302); self.send_header("Location", location); self.end_headers()
def log_message(self, *a): pass
return Index
def serve(handler):
srv = HTTPServer(("127.0.0.1", 0), handler)
threading.Thread(target=srv.serve_forever, daemon=True).start()
return srv.server_address[1]
def attempt(prompting):
port_b = serve(Upstream) # upstream (127.0.0.1)
port_a = serve(index(f"http://{USER}:{TOKEN}@127.0.0.1:{port_b}/pkg")) # index (localhost)
url = f"http://localhost:{port_a}/"
s = requests.Session()
s.auth = MultiDomainBasicAuth(prompting=prompting, index_urls=[url])
return s.get(url, timeout=5).status_code
Both attempts return 200. Extracting credentials embedded in a redirect URL requires no user interaction, so --no-input should not disable it.
Output
default (prompting=True) : 200
--no-input(prompting=False): 401
Code of Conduct
Description
I encountered an unexpected behavior when using
pip downloadusinguv run --no-project --with python -m pip. There was a strange behavior where I find out somehow--no-inputaffect the result of auth.Contexet
Download/install a package from a private index that answers with a 302 redirect whose Location carries embedded Basic-auth credentials (https://user:token@upstream/...) — a common setup with devpi in front of Artifactory/JFrog, and similar proxy configurations — while passing --no-input (e.g. in CI).
What happened
The download fails with 401 Unauthorized, but only when
--no-inputis used. The exact same command without--no-inputsucceeds.Root cause
When an index issues a cross-origin 302, the vendored requests session strips the Authorization header (should_strip_auth), so the upstream host receives no credentials and returns 401. pip's handle_401 (src/pip/_internal/network/auth.py) is supposed to recover: the embedded credentials are still present in resp.url, and _get_new_credentials() extracts URL-embedded credentials before it ever touches keyring or prompts.
Expected behavior
--no-inputshouldn't affect authentication and expect same behavior.pip version
25.3
Python version
3.13
OS
MacOS/Linux
How to Reproduce
When an index issues a cross-origin 302, the vendored requests session strips the Authorization header (should_strip_auth), so the upstream host receives no credentials and returns 401. pip's handle_401 (src/pip/_internal/network/auth.py) is supposed to recover: the embedded credentials are still present in resp.url, and _get_new_credentials() extracts URL-embedded credentials before it ever touches keyring or prompts.
The problem is that this recovery call is gated behind use_keyring:
--no-input is documented as "Disable prompting for input", but it also silently disables this non-interactive redirect-credential recovery.
Minimal reproduction
This example is generated with AI, but I did test it to confirm the behavior is same as what I experience.
Self-contained, stdlib + pip only (no external index needed). Two local servers stand in for "index → upstream"; only auth.prompting (what --no-input sets) differs between the two attempts:
Output
default (prompting=True) : 200
--no-input(prompting=False): 401
Code of Conduct