Skip to content

fix(formats): --trusted-host ignores the port on import and drops it on export - #3845

Merged
frostming merged 1 commit into
pdm-project:mainfrom
VXNCXNX:fix/trusted-host-port
Aug 17, 2026
Merged

fix(formats): --trusted-host ignores the port on import and drops it on export#3845
frostming merged 1 commit into
pdm-project:mainfrom
VXNCXNX:fix/trusted-host-port

Conversation

@VXNCXNX

@VXNCXNX VXNCXNX commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

--trusted-host with a port is ignored on import, and the port is dropped on
export, so an index on a non-default port does not round-trip.

requirements.txt with --trusted-host mirror.example.com:8443 and an index URL
carrying credentials:

before:
url = "https://user:pw@mirror.example.com:8443/simple"
verify_ssl = true            <- the trusted host was not matched
--trusted-host localhost     <- exported without the port

after:
url = "https://user:pw@mirror.example.com:8443/simple"
verify_ssl = false
--trusted-host mirror.example.com:8443

The import side is the one that bites: the source is silently recorded as
verify_ssl = true, so pdm then rejects a certificate the user explicitly said
to trust.

Cause

Two halves of the same round trip.

On import, the trusted host is compared by membership against two values:

netloc, host = parsed.netloc, parsed.hostname
for trusted in trusted_hosts:
    if trusted in (host, netloc):

host has no port, so mirror.example.com:8443 never equals it. netloc has
the port but also the userinfo, so for user:pw@mirror.example.com:8443 it does
not match either. It only works when the URL has a port and no credentials.

On export the port is thrown away:

host = urllib.parse.urlparse(url).hostname
lines.append(f"--trusted-host {host}\n")

pip does not treat a portless --trusted-host as covering an explicit port in
the URL, so the exported file does not reproduce the input.

The fix

Compare host and port explicitly, and keep the port on the way out. I took the
semantics from pip's own is_secure_origin and add_trusted_host rather than
guessing:

  • host compared case-insensitively, matching
    origin_host.lower() != secure_host.lower()
  • userinfo excluded, since pip compares parsed.hostname
  • a trusted host with no port matches any port, matching pip's
    secure_port is not None check and its wildcard-port mount

IPv6 literals are split with urlsplit so [::1]:8443 breaks on the right
colon, and are re-bracketed on export. A malformed port is treated as absent
rather than raising, so a bad value in a requirements file cannot abort the
import.

Verification

Reverting the matching logic to the membership test fails the import test:

E  At index 0 diff: {'url': 'https://user:pw@mirror.example.org:8443/simple', 'verify_ssl': True}
                 != {'url': 'https://user:pw@mirror.example.org:8443/simple', 'verify_ssl': False}

Reverting the export to .hostname fails the export tests, including the IPv6
one:

E  - --trusted-host mirror.example.org:8443
E  + --trusted-host mirror.example.org
E  AssertionError: assert '--trusted-host ::1' == '--trusted-host [::1]:8443'

tests/test_formats.py is 46 passed. On the wider suite the baseline is 1346
passed on a stashed tree and 1359 passed with this change, no failures either
way; the difference is the new tests.

Two tests are excluded from that count because they fail identically on an
unmodified tree here for environment reasons:
test_create_venv_in_project[venv-True] and the OIDC tests in
tests/cli/test_publish.py.

ruff check and ruff format --check are clean.

Pull Request Checklist

  • A news fragment is added in news/ describing what is new.
  • Test cases added for changed code.

Disclosure: written with AI assistance (Claude Code). I produced the before and after by running pdm import and pdm export, read pip's is_secure_origin and add_trusted_host from an installed pip to confirm the matching rules rather than relying on memory, and ran both mutation checks and the suite baseline myself.

@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.41%. Comparing base (5a6ea88) to head (9c1ecec).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3845      +/-   ##
==========================================
+ Coverage   88.36%   88.41%   +0.04%     
==========================================
  Files         121      121              
  Lines       13249    13256       +7     
  Branches     2251     2252       +1     
==========================================
+ Hits        11708    11720      +12     
+ Misses        969      966       -3     
+ Partials      572      570       -2     
Flag Coverage Δ
unittests 88.29% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@frostming
frostming merged commit fd378b1 into pdm-project:main Aug 17, 2026
25 checks passed
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