Skip to content

Commit 30f2c7d

Browse files
fix(files): 409 (not corrupt source) when a shared generated doc has no compiled artifact
1 parent f2fe99e commit 30f2c7d

2 files changed

Lines changed: 39 additions & 19 deletions

File tree

apps/sim/app/api/files/public/[token]/content/route.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { createLogger } from '@sim/logger'
22
import type { NextRequest } from 'next/server'
3+
import { NextResponse } from 'next/server'
34
import { getPublicFileContentContract } from '@/lib/api/contracts/public-shares'
45
import { parseRequest } from '@/lib/api/server'
5-
import { loadServableDocArtifact } from '@/lib/copilot/tools/server/files/doc-compile'
6+
import { resolveServableDoc } from '@/lib/copilot/tools/server/files/doc-compile'
67
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
78
import { enforcePublicFileRateLimit } from '@/lib/public-shares/rate-limit'
89
import { resolveActiveShareByToken } from '@/lib/public-shares/share-manager'
@@ -20,9 +21,10 @@ const logger = createLogger('PublicFileContentAPI')
2021
* shares. Disposition (inline vs attachment) is resolved from the file type by
2122
* {@link createFileResponse}; the public page's Download button uses `<a download>`.
2223
*
23-
* Generated office docs are stored as source; {@link loadServableDocArtifact}
24-
* swaps in their prebuilt compiled binary (read-only, never compiles). Uploaded
25-
* binaries pass through untouched.
24+
* Generated office docs are stored as source; {@link resolveServableDoc} swaps in
25+
* their prebuilt compiled binary (read-only, never compiles). Uploaded binaries
26+
* pass through untouched. A generated doc whose compiled artifact isn't built yet
27+
* returns 409 rather than serving raw source under a binary content type.
2628
*/
2729
export const GET = withRouteHandler(
2830
async (request: NextRequest, context: { params: Promise<{ token: string }> }) => {
@@ -42,11 +44,20 @@ export const GET = withRouteHandler(
4244
const { file } = resolved
4345
const raw = await downloadFile({ key: file.key, context: 'workspace' })
4446

45-
const artifact = file.workspaceId
46-
? await loadServableDocArtifact(file.workspaceId, raw, file.originalName)
47-
: null
48-
const buffer = artifact?.buffer ?? raw
49-
const contentType = artifact?.contentType ?? file.contentType
47+
const servable = file.workspaceId
48+
? await resolveServableDoc(file.workspaceId, raw, file.originalName)
49+
: ({ kind: 'passthrough' } as const)
50+
51+
if (servable.kind === 'unavailable') {
52+
logger.info('Public shared doc not yet compiled', { token, key: file.key })
53+
return NextResponse.json(
54+
{ error: 'This document is still being prepared. Please try again shortly.' },
55+
{ status: 409 }
56+
)
57+
}
58+
59+
const buffer = servable.kind === 'artifact' ? servable.buffer : raw
60+
const contentType = servable.kind === 'artifact' ? servable.contentType : file.contentType
5061

5162
logger.info('Public shared file served', { token, key: file.key, size: buffer.length })
5263

apps/sim/lib/copilot/tools/server/files/doc-compile.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -423,20 +423,29 @@ function bufferStartsWith(buffer: Buffer, magic: Buffer): boolean {
423423
}
424424

425425
/**
426-
* Resolve the servable binary for a stored doc WITHOUT compiling — for read-only
427-
* consumers (e.g. the public share route). An uploaded binary already carries its
428-
* format magic and is served as-is (returns null → caller uses the raw bytes); a
429-
* generated doc stored as source returns its prebuilt content-addressed artifact
430-
* when present, else null (caller falls back to raw). Never invokes E2B/isolated-vm.
426+
* How a read-only consumer (e.g. the public share route) should serve a stored doc
427+
* WITHOUT compiling:
428+
* - `passthrough` — serve the raw stored bytes as-is (a non-doc file, or an uploaded
429+
* binary that already carries its format magic).
430+
* - `artifact` — serve this prebuilt content-addressed compiled binary.
431+
* - `unavailable` — a generated doc stored as source whose compiled artifact does
432+
* not exist yet; the raw bytes are source, so serving them under the file's binary
433+
* content type would be corrupt. The caller should signal "not ready" instead.
431434
*/
432-
export async function loadServableDocArtifact(
435+
export type ServableDoc =
436+
| { kind: 'passthrough' }
437+
| { kind: 'artifact'; buffer: Buffer; contentType: string }
438+
| { kind: 'unavailable' }
439+
440+
export async function resolveServableDoc(
433441
workspaceId: string,
434442
storedBytes: Buffer,
435443
fileName: string
436-
): Promise<{ buffer: Buffer; contentType: string } | null> {
444+
): Promise<ServableDoc> {
437445
const fmt = await getE2BDocFormat(fileName)
438-
if (!fmt) return null
446+
if (!fmt) return { kind: 'passthrough' }
439447
const magic = fmt.ext === 'pdf' ? PDF_MAGIC : ZIP_MAGIC
440-
if (bufferStartsWith(storedBytes, magic)) return null
441-
return loadCompiledDocByExt(workspaceId, storedBytes.toString('utf-8'), fmt.ext)
448+
if (bufferStartsWith(storedBytes, magic)) return { kind: 'passthrough' }
449+
const artifact = await loadCompiledDocByExt(workspaceId, storedBytes.toString('utf-8'), fmt.ext)
450+
return artifact ? { kind: 'artifact', ...artifact } : { kind: 'unavailable' }
442451
}

0 commit comments

Comments
 (0)