Repository navigation
[Bug]: Private PR media doesn't load when the org enforces SAML SSO #14166
Description
Activity
- changed the title
[-][Bug]: Private PR media doesn't load when the org enforces SAML SSO[/-][+]Private PR media doesn't load when the org enforces SAML SSO[/+]on Sep 28, 2026 - changed the title
[-]Private PR media doesn't load when the org enforces SAML SSO[/-][+][Bug]: Private PR media doesn't load when the org enforces SAML SSO[/+]on Sep 28, 2026 Triage
Confirmed, and still present on main.
GitHubMediaFetchhas not changed sinced15210cd(the nightly in the report).githubMediaResponsesends theghtoken only to GitHub hosts and treats a 2xx as the media itself. An SSO sign-in page is200 text/html, so it never follows a redirect.user-attachmentsURLs are extensionless, the MIME fallback from the filename is empty, and the route returns 415 (apps/server/src/assets/GitHubMediaFetch.ts). That Effect succeeds, which is why the trace showsGitHubMediaFetch.githubMediaResponseexiting Success. The client falls back to the original URL only when signing the asset URL fails, not when the fetch returns 415, so the image or video stays broken. The same route serves PR descriptions, timeline comments, and review comments on web and desktop.This is the case #11706 did not cover. That change (which closed #6600) works when GitHub answers the token with a 302 to a signed object URL. An org that enforces SAML SSO does not do that for this web route, even when the same token is authorized for REST and GraphQL.
Bearerversustokendoes not matter; the server already sendsBearer, and both return the sign-in page. #11374 still assumes the redirect, so it would miss this too. Not a duplicate, and no newer release changes it.Rendering the attachment through
POST /markdownwith the repo ascontext, then fetching the signedprivate-user-imagesURL, is the right fix. A few constraints:- Do this only when a
user-attachmentsor legacyhub.lumenfield.work/<owner>/<repo>/assets/…fetch comes back200withtext/html. Raw and LFS URLs should stay on the current path. - The asset claim is only
{ url, cwd, expiresAt }. Auser-attachmentsURL does not name a repository, socontexthas to be resolved separately.gh repo viewfromcwdis the wrong repo when the checkout is a fork and the upload lives on the upstream PR. The PR'sowner/repois known when the body is fetched and is not passed into the asset resource today. - Accept only
https://private-user-images.githubusercontent.com/…from the rendered HTML, unescape&, and fetch it without theghtoken. That host is already outsideCREDENTIALED_HOSTS. - Those signatures last about 5 minutes. Bytes we have already streamed can keep the asset token's cache lifetime (60 minutes). Video range requests re-enter this route, so cache the signed URL for slightly less than 5 minutes instead of re-rendering on every seek. Do not log the signed URL.
Opening the PR on github.com in a browser that already has an SSO session remains the workaround.
- Do this only when a
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Sep 28, 2026
Before submitting
Area
apps/server
Steps to reproduce
ghon the T3 server host is logged in with an SSO-authorized token (gh api repos/<owner>/<repo>returns 200).https://github.com/user-attachments/assets/<id>).Expected behavior
The image or video loads through the server's
ghcredential, as it does for private repositories since #11706.Actual behavior
The media shows as unavailable. Server trace: the upstream
GET https://github.com/user-attachments/assets/<id>returns 200, then/api/assets/*returns 415.GitHub never redirects to the signed storage URL. With the
ghtoken it returns200 text/html, which is the organization's SSO sign-in page ("Sign in to ", withsaml/initiate?return_to=…user-attachments…).githubMediaResponsethen rejects the HTML as a non-media content type.github.com/user-attachments/assets/<id>Authorization: Bearer <gh token>text/html(SSO sign-in page)Authorization: token <gh token>text/html(SSO sign-in page)I reproduced this with two separate
ghOAuth tokens (gho_, scopesrepo, read:org, gist) on two machines. Both work for REST and GraphQL calls on the repo. The token is recognized (404 becomes 200), but GitHub seems to require an SSO browser session for this web route, which a token can't provide.Related: #6600 (same symptom, closed as fixed by #11706; that fix works when GitHub redirects to storage, which SSO-enforcing orgs don't). The open #11374 uses the same request-and-expect-a-redirect approach for GitHub, so it would fail the same way.
Impact
Major degradation or frequent failure
Version or commit
0.0.43-nightly.20260928.2375(d15210cd3da7), on both server and desktopEnvironment
macOS desktop client connected to a remote Linux x64 T3 server (systemd user service),
gh2.x authenticated on the serverLogs or stack traces
Screenshots, recordings, or supporting files
No response
Workaround
Open the PR on github.com in a browser with an active SSO session.
Possible fix: GitHub's markdown render API returns a signed URL for an attachment when given the repo as context, and that URL loads without a credential, under SSO too:
GraphQL
bodyHTMLon the PR also contains these signed URLs. The signatures expire after about 5 minutes (exp - nbf = 300).In
GitHubMediaFetch.githubMediaResponse, when auser-attachmentsrequest returns200 text/html, the server could render the URL through/markdown, fetch the signedprivate-user-imagesURL without the token, and stream it. Caching the signed URL for about 4 minutes would keep video range requests from re-rendering each time.Investigated with Claude Code (Claude Opus 5.5).