From c4cbe54e30be5223219040991c9582ab09ee6d03 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 10 Jun 2026 10:05:29 +0200 Subject: [PATCH] Treat remote 404 as missing preview in readRemoteFile Navigating Pear Issues logged a RelayFileApiError 404 from the integrations:read-remote-file IPC handler. The Issues store enriches each Linear issue with its linked GitHub record by reading a synthetic path (/github/repos/{owner}/{repo}/issues/{number}.json) derived from Linear sync metadata. Because historical provider records are not downloaded locally, that file often does not exist remotely and the RelayFile API returns 404. The renderer already recovers (readGithubLink wraps the read in .catch(() => null) and falls back to a basic link), but readRemoteFile threw on the 404, so Electron logged the rejected IPC handler regardless. Make readRemoteFile catch a 404 and return a { kind: 'missing' } preview, mirroring how the local filesystem path (readTextPreview) already reports absent files. Both callers act only on kind === 'text', so they degrade cleanly: missing GitHub enrichment falls back to the basic link, and a race where an issue file vanishes between listing and read skips that issue rather than failing the board. Co-Authored-By: Claude Opus 4.8 --- src/main/integrations.test.ts | 19 +++++++++++++++++++ src/main/integrations.ts | 25 +++++++++++++++++++++---- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/main/integrations.test.ts b/src/main/integrations.test.ts index 3decd711..3726cc1e 100644 --- a/src/main/integrations.test.ts +++ b/src/main/integrations.test.ts @@ -953,6 +953,25 @@ describe('IntegrationsManager', () => { expect(mock.integrationMountManager.ensureMounted).not.toHaveBeenCalled() }) + it('returns a missing preview when an in-scope remote read 404s instead of rejecting', async () => { + // Historical provider records (e.g. the GitHub issue JSON synthesized from + // Linear sync metadata) are not downloaded locally, so an in-scope read can + // legitimately 404. It must degrade to a missing preview rather than reject + // the IPC handler. + mock.relayClient.readFile.mockImplementationOnce(async (_workspaceId: string, path: string) => { + mock.readFileCalls.push({ workspaceId: _workspaceId, path }) + throw Object.assign(new Error('not found'), { status: 404, code: 'not_found' }) + }) + + const manager = new IntegrationsManager() + const preview = await manager.readRemoteFile( + 'project-1', + '/slack/channels/C123/messages/1713220123_001100.json' + ) + + expect(preview).toMatchObject({ kind: 'missing', size: 0 }) + }) + it('rejects targeted remote file reads outside the project integration scope', async () => { const manager = new IntegrationsManager() diff --git a/src/main/integrations.ts b/src/main/integrations.ts index 2d950257..7acc03a6 100644 --- a/src/main/integrations.ts +++ b/src/main/integrations.ts @@ -1060,10 +1060,27 @@ export class IntegrationsManager { throw new Error('Integration remote file is outside this project integration scope') } - return this.withIntegrationRemoteHandle(async (handle) => { - const file = await handle.client().readFile(handle.workspaceId, path) - return remoteFileReadToPreview(file) - }) + try { + return await this.withIntegrationRemoteHandle(async (handle) => { + const file = await handle.client().readFile(handle.workspaceId, path) + return remoteFileReadToPreview(file) + }) + } catch (error) { + // A remote 404 means the file was never synced locally (e.g. historical + // provider records that are not downloaded) or was removed between a + // directory listing and this read. Mirror the local filesystem behavior + // (readTextPreview) by reporting it as a missing preview instead of + // rejecting the IPC handler, so best-effort enrichment readers degrade + // gracefully rather than logging a handler error. + if (isHttpStatus(error, 404)) { + return { + kind: 'missing', + content: error instanceof Error ? error.message : 'Remote file not found', + size: 0 + } + } + throw error + } } async listRemoteDirectory(projectId: string, remotePath: string): Promise {