Skip to content

fix(pull-requests): render private GitHub images in PR bodies - #8446

Open
Xanacas wants to merge 5 commits into
pingdotgg:mainfrom
Xanacas:t3code/fix-pr-image-rendering
Open

fix(pull-requests): render private GitHub images in PR bodies#8446
Xanacas wants to merge 5 commits into
pingdotgg:mainfrom
Xanacas:t3code/fix-pr-image-rendering

Conversation

@Xanacas

@Xanacas Xanacas commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Images embedded in pull request bodies rendered as broken placeholders: GitHub serves github.com/user-attachments/assets/… uploads only to an authenticated viewer, and the browser sends no GitHub cookies from the app's origin (verified: unauthenticated fetch → 404, with a gh token → 200).

Fixed by routing those URLs through the existing signed asset flow. The markdown image classifier in client-runtime returns a new GitHubAttachment source, clients request a signed /api/assets URL for it over the authenticated WebSocket, and the server resolves GitHub's redirect with the gh CLI token (cached, single-flight) and answers 302 to the short-lived signed storage URL. Image bytes flow from storage straight to the browser; the token never leaves the server. Claim expiry is bucketed so repeat issuances stay byte-identical and browser caches hit. Web and mobile both render through the shared classifier; public-repo uploads resolve anonymously if gh is not authenticated.

Verified end to end in a local web run against a private-repo PR — before/after screenshots in this comment.

Changes made by Claude Fable 5 (Claude Code).

🤖 Generated with Claude Code

Note

Add github-attachment asset type to render private GitHub images in PR bodies

  • Introduces a new github-attachment variant in AssetResource, asset claims, and ResolvedAsset, with strict validation via isGitHubUserAttachmentUrl in assets.ts
  • Server issues signed asset URLs for GitHub attachment URLs and resolves them at request time: GitHubAttachmentProxy uses cached gh auth token to follow GitHub's redirect and respond with a 302 to the storage URL (private max-age=240), or 404 on failure
  • Web ChatMarkdown.tsx and mobile ThreadFeed.tsx markdown renderers now classify GitHub attachment URLs and load them through the signed asset proxy, falling back to the original direct URL if signing or the signed URL fails
  • classifyMarkdownImageSource in markdownImages.ts and attachmentFromLine in pullRequestMarkdown.logic.ts now use the shared validator for GitHub attachment link detection
  • Risk: issueAssetUrl rejects non-allowlisted remote URLs with AssetRemoteUrlValidationError; any caller passing an unvalidated URL that previously fell through to other handling will now receive this error

Macroscope summarized 42a2295.

GitHub serves user-attachments uploads only to an authenticated viewer, so
images in PR bodies rendered as broken placeholders in the app. Clients now
route those URLs through the signed asset route; the server resolves GitHub's
redirect with the gh CLI token and answers 302, so bytes flow from signed
storage straight to the browser and the token never leaves the server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 49f8d361-abe9-4c0c-8f31-67ab21c55615

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 27, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect Service Conventions: one finding — the new GitHubAttachmentProxy service adds backend behavior with no focused test. Service definition order, make/layer exports, Foo["Service"] typing, environment-based dependency acquisition, and the new Schema.TaggedErrorClass all match the conventions.

Posted via Macroscope — Effect Service Conventions

Comment on lines +48 to +67
const resolveAttachmentLocation: GitHubAttachmentProxy["Service"]["resolveAttachmentLocation"] = (
url,
) =>
Effect.gen(function* () {
const token = yield* readToken;
const request = HttpClientRequest.get(url, {
headers: token === null ? {} : { authorization: `token ${token}` },
});
const response = yield* httpClient
.execute(request)
.pipe(Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }));
if (response.status < 300 || response.status >= 400) return null;
const location = response.headers["location"];
return location !== undefined && location.startsWith("https://") ? location : null;
}).pipe(
Effect.tapError((cause) =>
Effect.logWarning("Failed to resolve a GitHub attachment.", { url, cause }),
),
Effect.orElseSucceed(() => null),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New backend behavior lands without a focused test: nothing exercises resolveAttachmentLocation, including the security-relevant guards (only 3xx responses are followed, and only an https:// location is returned) or the no-token path. Consider adding GitHubAttachmentProxy.test.ts with an HttpClient test layer (as AnalyticsService.test.ts / CliTokenManager.test.ts do) plus a stub GitHubCli layer, covering: 302 with an https location resolves, non-3xx status yields null, a non-https location yields null, and a failing gh auth token still issues the request unauthenticated.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Focused tests added in bd78fb8 (GitHubAttachmentProxy.test.ts): stub GitHubCli + HttpClient layers cover the token header and cached read, the anonymous no-token fallback, and null answers for non-3xx responses and non-https locations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1da4829ea2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +83 to +85
return isGitHubUserAttachmentUrl(source)
? { _tag: "GitHubAttachment", url: source }
: { _tag: "Direct", uri: source };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve direct loading against older servers

When app.t3.codes or the mobile app updates before a connected environment, this reclassifies every GitHub user-attachment URL and sends the new github-attachment resource to assets.createUrl; an older server's AssetResource decoder rejects that unknown tag. Consequently, even public attachments that previously rendered directly become unavailable during version skew. Gate the proxy on a server capability or fall back to the original direct URL when URL creation fails.

AGENTS.md reference: AGENTS.md:L19-L21

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in fbba68e: both clients now fall back to loading the GitHub URL directly when signed-URL creation fails (older server rejecting the tag) or when no environment is available — public uploads keep rendering, private ones degrade to the unavailable chip they showed before this PR.

@macroscopeapp

macroscopeapp Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This adds a credentialed GitHub redirect proxy and a new asset contract across server, WebSocket, web, and mobile paths, with security-sensitive token and redirect handling. The mobile fallback path also has an unresolved load-error concern.

Not approved because:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

Route handlers resolve services at serve time, so providing the proxy to
makeRoutesLayer never reached the request fiber and every proxied image
answered 404. Caught in an integrated web pass; unit tests could not see it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Xanacas

Xanacas commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Before/after from an integrated web pass against a private-repo PR (Vantisgo, PR body with three user-attachments screenshots):

Before (images 404 as broken placeholders):

before

After (rendered through the signed asset proxy; all three images load with real dimensions):

after

The integrated pass also caught a layer-wiring bug the unit tests could not see: route handlers resolve services at serve time, so the proxy layer had to be provided on the server runtime context rather than on makeRoutesLayer (fixed in 1a12535).

Xanacas and others added 2 commits August 27, 2026 22:56
Stub GitHubCli and HttpClient layers cover the token header and its cached
read, the anonymous fallback when gh has no token, and the null answers for
non-redirect responses and non-https locations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…available

An older server rejects the github-attachment resource during version skew,
and a markdown render without an environment has no proxy at all. Both now
fall back to loading the GitHub URL directly — public uploads keep working,
private ones degrade to the unavailable chip instead of regressing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
: { _tag: "workspace-file", threadId: props.threadId, path: imageSource.path }
}
sourceKey={imageSource._tag === "GitHubAttachment" ? imageSource.url : imageSource.path}
{...(imageSource._tag === "GitHubAttachment" ? { fallbackUri: imageSource.url } : {})}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium threads/ThreadFeed.tsx:1635

When the signed URL issued for a GitHub attachment returns a load error, the image is rendered as Image unavailable even though fallbackUri is loadable. fallbackUri is only selected when useAssetUrlState fails before loading; update ThreadMarkdownImageView to retry fallbackUri from onError before marking the image unavailable, matching the web renderer.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadFeed.tsx around line 1635:

When the signed URL issued for a GitHub attachment returns a load error, the image is rendered as `Image unavailable` even though `fallbackUri` is loadable. `fallbackUri` is only selected when `useAssetUrlState` fails before loading; update `ThreadMarkdownImageView` to retry `fallbackUri` from `onError` before marking the image unavailable, matching the web renderer.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the web-scoped changes (ChatMarkdown.tsx, pullRequest/pullRequestMarkdown.logic.ts) for shared-primitive use, Tailwind/CSS ownership, and environment routing.

Environment routing is correct: ChatMarkdownAssetImage requires an explicit environmentId, ChatMarkdown resolves it from threadRef?.environmentId ?? explicitEnvironmentId ?? null with no active-environment fallback, and every PullRequestMarkdown / PullRequestMarkdownEditor call site threads the owning environment explicitly.

One consistency finding on the new no-environment GitHub attachment branch (inline).

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/ChatMarkdown.tsx Outdated
The no-environment branch rendered a raw img with inline geometry and the
browser's broken-image glyph on failure; it now shares the block layout and
the unavailable chip of the proxied path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant