From b95c6ce1a26d40fcd4fbbba78fdfcb5ba113636f Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:06:46 +0000 Subject: [PATCH] fix(workspaces): return 404 for missing workspace files requireActiveWorkspaceItemRow now throws a typed WorkspaceItemNotFoundError. The file preview and file content routes map it to a 404, so a request for a deleted item does not capture an exception or return a 500. Co-Authored-By: Claude Opus 5.5 Generated-By: PostHog Desktop Task-Id: 0aa3d0b8-dd54-49d4-9c4c-239fb752b47b --- .../files/workspace-file-routes.test.ts | 47 +++++++++++++++++++ .../persistence/workspace-postgres-support.ts | 9 +++- ...aces.$workspaceId.files.$itemId.content.ts | 4 ++ ...aces.$workspaceId.files.$itemId.preview.ts | 4 ++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/features/workspaces/files/workspace-file-routes.test.ts diff --git a/src/features/workspaces/files/workspace-file-routes.test.ts b/src/features/workspaces/files/workspace-file-routes.test.ts new file mode 100644 index 00000000..95fb655b --- /dev/null +++ b/src/features/workspaces/files/workspace-file-routes.test.ts @@ -0,0 +1,47 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const getSessionFromRequest = vi.hoisted(() => vi.fn()); +const readWorkspaceFilePreview = vi.hoisted(() => vi.fn()); +const readWorkspaceFileSource = vi.hoisted(() => vi.fn()); +const recordOperationalFailure = vi.hoisted(() => vi.fn()); + +vi.mock("cloudflare:workers", () => ({ env: {} })); +vi.mock("#/lib/auth-queries.server", () => ({ getSessionFromRequest })); +vi.mock("#/features/workspaces/persistence/workspace-files", () => ({ + readWorkspaceFilePreview, + readWorkspaceFileSource, +})); +vi.mock("#/integrations/observability/operational-events", () => ({ recordOperationalFailure })); +vi.mock("#/integrations/posthog/server-context", () => ({ + getTelemetryRequestDetails: () => ({}), +})); + +import { WorkspaceItemNotFoundError } from "#/features/workspaces/persistence/workspace-postgres-support"; +import { handleWorkspaceFileContent } from "#/routes/api/v1/workspaces.$workspaceId.files.$itemId.content"; +import { handleWorkspaceFilePreview } from "#/routes/api/v1/workspaces.$workspaceId.files.$itemId.preview"; + +beforeEach(() => { + vi.clearAllMocks(); + getSessionFromRequest.mockResolvedValue({ user: { id: "user-1" } }); +}); + +describe("workspace file routes", () => { + it.each([ + ["preview", handleWorkspaceFilePreview, readWorkspaceFilePreview], + ["content", handleWorkspaceFileContent, readWorkspaceFileSource], + ])( + "returns 404 without capturing a failure when the %s item is missing", + async (_, handler, read) => { + read.mockRejectedValue(new WorkspaceItemNotFoundError()); + + const response = await handler( + new Request("https://thinkex.test/file"), + "workspace-1", + "missing-item", + ); + + expect(response.status).toBe(404); + expect(recordOperationalFailure).not.toHaveBeenCalled(); + }, + ); +}); diff --git a/src/features/workspaces/persistence/workspace-postgres-support.ts b/src/features/workspaces/persistence/workspace-postgres-support.ts index 9063a0b2..17b34a46 100644 --- a/src/features/workspaces/persistence/workspace-postgres-support.ts +++ b/src/features/workspaces/persistence/workspace-postgres-support.ts @@ -33,6 +33,13 @@ export type Transaction = Parameters[0]>[0]; export type QueryExecutor = Database | Transaction; export type ItemRow = typeof workspaceItems.$inferSelect; +export class WorkspaceItemNotFoundError extends Error { + constructor() { + super("Workspace item not found."); + this.name = "WorkspaceItemNotFoundError"; + } +} + export async function withWorkspaceTransaction(run: (transaction: Transaction) => Promise) { return await withDb((db) => db.transaction(run)); } @@ -114,7 +121,7 @@ export async function requireActiveWorkspaceItemRow( itemId: string, ) { const row = await getActiveWorkspaceItemRow(db, workspaceId, itemId); - if (!row) throw new Error("Workspace item not found."); + if (!row) throw new WorkspaceItemNotFoundError(); return row; } diff --git a/src/routes/api/v1/workspaces.$workspaceId.files.$itemId.content.ts b/src/routes/api/v1/workspaces.$workspaceId.files.$itemId.content.ts index 9fa88986..1bea5b7d 100644 --- a/src/routes/api/v1/workspaces.$workspaceId.files.$itemId.content.ts +++ b/src/routes/api/v1/workspaces.$workspaceId.files.$itemId.content.ts @@ -2,6 +2,7 @@ import { env } from "cloudflare:workers"; import { createFileRoute } from "@tanstack/react-router"; import { readWorkspaceFileSource } from "#/features/workspaces/persistence/workspace-files"; +import { WorkspaceItemNotFoundError } from "#/features/workspaces/persistence/workspace-postgres-support"; import { WorkspaceForbiddenError } from "#/features/workspaces/server/permissions"; import { apiError, apiFailure, getRequestId } from "#/lib/api/http"; import { getSessionFromRequest } from "#/lib/auth-queries.server"; @@ -50,6 +51,9 @@ async function handleWorkspaceFileContent(request: Request, workspaceId: string, "You do not have permission to view files in this workspace.", ); } + if (error instanceof WorkspaceItemNotFoundError) { + return apiError(requestId, 404, "FILE_NOT_FOUND", "This workspace file no longer exists."); + } return apiFailure({ cause: error, diff --git a/src/routes/api/v1/workspaces.$workspaceId.files.$itemId.preview.ts b/src/routes/api/v1/workspaces.$workspaceId.files.$itemId.preview.ts index 6f1cf842..cd0168ee 100644 --- a/src/routes/api/v1/workspaces.$workspaceId.files.$itemId.preview.ts +++ b/src/routes/api/v1/workspaces.$workspaceId.files.$itemId.preview.ts @@ -2,6 +2,7 @@ import { env } from "cloudflare:workers"; import { createFileRoute } from "@tanstack/react-router"; import { WORKSPACE_FILE_PREVIEW_CONTENT_TYPE } from "#/features/workspaces/files/workspace-file-preview.constants"; import { readWorkspaceFilePreview } from "#/features/workspaces/persistence/workspace-files"; +import { WorkspaceItemNotFoundError } from "#/features/workspaces/persistence/workspace-postgres-support"; import { WorkspaceForbiddenError } from "#/features/workspaces/server/permissions"; import { apiError, apiFailure, getRequestId } from "#/lib/api/http"; import { getSessionFromRequest } from "#/lib/auth-queries.server"; @@ -66,6 +67,9 @@ async function handleWorkspaceFilePreview(request: Request, workspaceId: string, "You do not have permission to view files in this workspace.", ); } + if (error instanceof WorkspaceItemNotFoundError) { + return apiError(requestId, 404, "PREVIEW_NOT_FOUND", "This workspace file no longer exists."); + } return apiFailure({ cause: error,