fix(cloud): build login url from the bound auth listener port - #10516
Merged
Conversation
when the login port is already in use, setupAuthListener falls back to the next free port, but getLoginUrl rebuilt the url from the requested port and resolved a stale, server-less map entry. the browser was told to post the token to a port with no server behind it, so the token never arrived and bit login hung indefinitely. thread the port the listener actually bound to into getLoginUrl, drop the stale entry on the EADDRINUSE retry, and forward clientId/skipConfigUpdate/ cloudDomain to the retried listener (previously dropped on every bump). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getLoginUrl's second branch (no listener registered yet for the requested
port) paired the requested port with the bound listener's clientId, so the
url pointed at a port with nothing behind it whenever setupAuthListener
bumped. this is the path the workspace ui login link takes via the graphql
loginUrl query.
verified with 8888 occupied: `{loginUrl}` advertised 8888 while bit start
listened on 8889; now advertises 8889.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
deviceName is interpolated raw from --machine-name or os.hostname(), so a machine named `home & away #1` produced an invalid url: the '#' turned the rest into a fragment (dropping os), the '&' forged a bogus param, and raw spaces broke the url when handed to open() or copy-pasted. encodeURI() could not fix this — it leaves '&', '#' and '+' intact. encode each value with encodeURIComponent instead, and route both getLoginUrl branches through a single buildLoginUrl helper so the encoding and the "advertise the bound port" invariant cannot drift apart again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
luvkapur
marked this pull request as ready for review
July 22, 2026 19:20
luvkapur
enabled auto-merge (squash)
July 22, 2026 19:21
davidfirst
approved these changes
Jul 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
bit loginstarts a local callback server and then hands the browser a url telling it which port to post the token back to. When that port is already taken, the two silently diverge and login never completes.setupAuthListenerregisters its map entry optimistically — beforelisten()runs:If
listen()then fails withEADDRINUSE, the retry bindsport + 1, but the stale, server-less entry for the original port is never removed.getLoginUrlafterwards recomputes the port from the requested value rather than the one that was actually bound, finds that stale entry, and advertises it:The same defect exists in
getLoginUrl's other branch — when no listener is registered yet for the requested port, it pairs the requested port with the bound listener's clientId:That is the path the workspace ui login link takes, via the graphql
loginUrlquery.Either way the browser posts the token to a port with no bit server behind it. The token is lost, the callback never fires, and
bit loginhangs indefinitely — from the user's side it looks like the login page did nothing.The default login port is 8889 (
new LoginCmd(cloudMain, 8889)), so this reproduces on any machine already running something there. NoteCloudMain.DEFAULT_PORT = 8888is only the fallback for the workspace's ownsetupAuthListener()— it is not thebit logindefault.Repro — occupy the login port, then log in:
A second, quieter bug in the same path: the
EADDRINUSEretry dropsclientId,skipConfigUpdateandcloudDomain. A bumped listener therefore rewrites.npmrceven when the user passed--skip-config-update, and re-mints a clientId that no longer matches the one advertised.Unencoded url params
deviceNameis interpolated raw from--machine-nameoros.hostname(). A machine namedhome & away #1yields a url that is not valid at all:The
#turns everything after it into a fragment, soosis silently dropped; the&forges a bogus param; and the raw spaces break the url when handed toopen()or copy-pasted.encodeURI()— used on one of the two branches — cannot fix this either, since it leaves&,#and+intact.Fix
login()(cli) and ingetLoginUrl's setup branch (graphql / workspace ui). in the cli path this corrects the advertisedclientIdtoo, since it now resolves the real listener's entryEADDRINUSEretry, so no caller can resolve a port with nothing behind itclientId/skipConfigUpdate/cloudDomainto the retried listenerencodeURIComponent, and route bothgetLoginUrlbranches through a singlebuildLoginUrlhelper so the encoding and the "advertise the bound port" invariant cannot drift apart againNo behavior change when the requested port is free.
Verification
Built from source and exercised end to end with
bit login --no-browser, simulating the browser callback withcurlagainst the port the url advertises. Identical test, both builds:port=8889port=889088908890'squat'200 Login successfulbit loginoutcome✔ Logged in as <user>Workspace ui path, via
bit startwith 8888 occupied and the{loginUrl}graphql query:loginUrladvertisesport=8888port=8889bit startlistening on88898889Also verified:
--port 8085with 8085 occupied — advertised8085/ bound8086before, both8086after--machine-name 'home & away #1 café+wifi'— before: url truncated at the space,deviceNamecame back ashome,oslost entirely. after:deviceName=home%20%26%20away%20%231%20caf%C3%A9%2Bwifi, round-trips byte-for-byte,osintact, no fragmentmapKeys=[8889,8890], withgetLoginUrlresolving the server-less8889Note on #10225
Refs #10225 — not confirmed to be the same bug, so this intentionally does not auto-close it. That report describes the login button not being clickable, which is a render-side symptom on the
bit.dev/bit-loginpage; the failure fixed here happens after the click. Both present as "login does nothing", so this is worth ruling in or out with the reporter before closing the issue.🤖 Generated with Claude Code