Skip to content

[FEATURE] Cross-protocol fallback with custom port: port mismatch across SSH/HTTPS #786

@edenfunf

Description

@edenfunf

Problem

When --allow-protocol-fallback is enabled and a dependency has a custom port, _build_repo_url passes the same dep_ref.port to both build_ssh_url and build_https_clone_url. For servers where SSH and HTTPS run on different ports, the cross-protocol fallback produces an incorrect URL.

Concrete scenario: Bitbucket Datacenter

Bitbucket DC defaults: SSH on port 7999, HTTPS on port 7990.

- ssh://git@bitbucket.corp.com:7999/project/repo.git
Attempt URL produced Result
SSH ssh://git@bitbucket.corp.com:7999/project/repo.git Correct
HTTPS fallback https://bitbucket.corp.com:7999/project/repo.git Wrong — HTTPS is on 7990, not 7999

Other affected scenarios

Setup SSH attempt HTTPS fallback Result
GitLab (SSH:2222, HTTPS:443) ssh://host:2222/... https://host:2222/... Hits SSH daemon
Same-port server (e.g. Gitea:3000) Works by accident

Current mitigations

Strict mode (default since #778) prevents this from manifesting for explicit ssh:// and https:// URLs — only the declared protocol is attempted. The bug only surfaces when users explicitly opt in via --allow-protocol-fallback or APM_ALLOW_PROTOCOL_FALLBACK=1.

The test test_https_attempt_preserves_same_port_across_protocols (test_auth_scoping.py:423) codifies "same port across protocols" as intended behavior — which is correct for same-port servers but wrong for the Bitbucket DC scenario.

Root cause

DependencyReference has a single port: Optional[int] field shared across all protocols. There is no ssh_port / https_port distinction. TransportAttempt and TransportSelector have no port awareness — port handling lives entirely in _build_repo_url which blindly passes dep_ref.port to whichever builder it calls.

Design space

The fundamental issue: when crossing protocols, we cannot guess the correct port for the other protocol.

Option A: Warn + multi-port fallback cascade

When dep_ref.port is set and cross-protocol fallback triggers:

  1. Try with same port (covers same-port servers)
  2. If that fails, try with default port (covers servers where the other protocol uses default port)
  3. Emit a warning: "Custom port {port} was specified for {scheme}://; the fallback protocol may use a different port."

Pro: no schema change. Con: doesn't cover BB DC (HTTPS:7990 is neither 7999 nor 443).

Option B: fallback field in object form

- git: ssh://git@bitbucket.corp.com:7999/project/repo.git
  fallback: https://bitbucket.corp.com:7990/project/repo.git
  ref: v1.0

Pro: fully explicit, covers any URL difference (not just port — also path differences for ADO, proxies, etc.). Con: schema expansion.

Option C: Per-protocol port fields in object form

- git: bitbucket.corp.com/project/repo
  ssh_port: 7999
  https_port: 7990

Pro: explicit. Con: only addresses port, not other URL differences; requires host field to be meaningful; less general than Option B.

Option D: Status quo + documentation

Strict mode already prevents the problem for explicit URLs. Document that --allow-protocol-fallback with custom ports may produce incorrect fallback URLs, and recommend pinning the protocol that matches the port.

Pro: zero code change. Con: doesn't help users who genuinely need fallback across different ports.

Files involved

  • src/apm_cli/deps/github_downloader.py_build_repo_url (line 664), _clone_with_fallback
  • src/apm_cli/deps/transport_selection.pyTransportSelector.select, TransportAttempt (no port awareness)
  • src/apm_cli/utils/github_host.pybuild_ssh_url, build_https_clone_url
  • src/apm_cli/models/dependency/reference.py — single port field

Context

Follow-up from PR #665 review. The "same port across protocols" behavior was a deliberate choice in #665 to fail loudly rather than silently hitting a different service. Strict-by-default transport (#778) subsequently removed most of the exposure. This issue tracks the residual gap for the --allow-protocol-fallback path.

Refs: #661, #665, #731, #778

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions