pingdotgg / pingdotgg/t3code

[Bug]: Nightly auto-update on macOS stalls at "Downloading (0%)" behind a TLS-inspecting proxy (Netskope)

Open
#4,812 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

Area: apps/desktop

Impact: Major degradation or frequent failure

Version: 0.0.30-nightly.20260728.929, updating to 0.0.30-nightly.20260728.933

Environment: macOS 26.5.2 (25F84), Apple Silicon, arm64 build, nightly channel. Managed device running the Netskope client, which proxies and TLS-inspects all traffic via a system extension.


Steps to reproduce

  1. Run the arm64 nightly build on a machine where Netskope (or any similar TLS-inspecting proxy) is active.
  2. Trigger "Check for Updates". The check itself succeeds and a new nightly is offered.
  3. Start the download.

Expected behavior

The update downloads and installs, or fails with an error that says what actually went wrong.

Actual behavior

The button sits at "Downloading (0%)" indefinitely. Nothing is ever written to the updater cache. Eventually it surfaces:

Electron updater failed to download the update on channel nightly.

which is ElectronUpdaterDownloadUpdateError from apps/desktop/src/electron/ElectronUpdater.ts. That message doesn't give you anything to act on, and there's no visible log file to dig into — the app doesn't seem to write an electron-log main.log anywhere I could find.

What I found digging into it

The update check works fine, so the feed is reachable. It's specifically the asset download that dies.

~/Library/Caches/t3code-updater/pending/ stays completely empty for the whole "download". Zero bytes, not a partial file.

lsof on the network helper process during the stall shows connections to the release CDN that were opened and then closed without transferring anything:

TCP <local>:<port>-><release-assets blob storage>:443     (CLOSED)
TCP <local>:<port>-><release-assets.githubusercontent.com>:443 (CLOSED)

Netskope's own log shows the app's flows being opened with no hostname attached, which I assume is because Chromium does its own DNS resolution and the network extension never sees the SNI:

Open flow TCP com.t3tools.t3code.helper[...] remote: <release-assets blob storage>:443, hostname (null), is not bound
Open flow TCP com.t3tools.t3code.helper[...] remote: <release-assets.githubusercontent.com>:443, hostname (null), is not bound

By contrast, curl on the same machine, through the same proxy, downloading the exact same asset, works perfectly — 244 MB at about 8 MB/s, and the file matches the published asset size byte for byte. So the host isn't blocked and there's no policy denying the download. Something in the in-app download path specifically doesn't survive the proxy.

I want to be upfront that I did not prove the root cause — I only got as far as "connections open, close, and transfer nothing." My guess is the differential downloader, since it issues a lot of small ranged requests against a presigned URL, and that's a much more fragile pattern under an inspecting proxy than one big sequential GET. But that is a guess, not something I confirmed.

One thing I did notice while looking at the source: in apps/desktop/src/updates/DesktopUpdates.ts (lines 403 and 737), differential download is only turned off for isArm64HostRunningIntelBuild. On a native arm64 build it stays enabled, and there's no user-facing setting to turn it off, so there's no way to test that theory from inside the app.

Workaround

Download the DMG from the releases page and install over the top. Signature checks out (Developer ID Application: T3 Tools, Inc. (ARK85ZXQ4Z), notarized, passes codesign --verify --deep --strict), settings in ~/Library/Application Support/t3code are preserved.

Worth mentioning: I also had a stale current.blockmap and a leftover update.zip sitting in ~/Library/Caches/t3code-updater/, plus an orphaned ~/Library/Caches/t3-code-desktop-updater/ from what looks like an older app id, holding old zips going back months. Nearly a gigabyte of dead cache. Clearing all of it did not fix the download, so it wasn't the cause, but the old-app-id directory never being cleaned up seems like a separate small bug.

Suggestions

A few things that would have helped, roughly in order of how useful I think they'd be:

  • Fall back to a plain full download if the differential path makes no progress. Right now a stall just hangs at 0% forever.
  • Add a timeout so it fails in reasonable time instead of sitting there.
  • Surface the underlying error in the UI, or write updater logs somewhere findable. cause is already captured on the error class but never seems to reach the user.
  • Expose disableDifferentialDownload as a setting. It's already plumbed through ElectronUpdater.ts, just not reachable.

Happy to test a build or pull more logs if that's useful. I can reproduce this reliably.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the macOS nightly failure with the Netskope proxy, then inspect apps/desktop/src/updates/DesktopUpdates.ts around lines 403 and 737 and apps/desktop/src/electron/ElectronUpdater.ts. Compare the differential-download path with the reported cache and helper-process behavior. Done means the update completes or fails promptly with an actionable error under the described environment.

Written by the indexing model from the issue text.

Assessment

Tech stack
electron, macos, typescript
Domain
desktop, networking, release
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.