Skip to content

Commit fc2ded7

Browse files
committed
fix(files): count the document body against the export limit
The 250 MB export cap measured only the embedded assets' declared sizes. The markdown body was downloaded with no limit and never counted, so a large document with modest attachments cleared the check and still produced a zip well over the stated limit, materialized whole in memory. The body is the largest single entry in most bundles, so excluding it left the limit unenforced against the item most able to exceed it. It is now capped on read and counted alongside its assets, and the message names both. Follow-up to #5995, which introduced the asset caps without extending them to the body.
1 parent bf376dd commit fc2ded7

2 files changed

Lines changed: 30 additions & 3 deletions

File tree

apps/sim/app/api/files/export/[id]/route.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,26 @@ describe('markdown export bundling', () => {
116116
expect(mockDownloadFile).toHaveBeenCalledTimes(1)
117117
})
118118

119+
it('counts the document body against the export limit, not just its assets', async () => {
120+
// Assets alone sit under the cap; the body is what carries the bundle over it.
121+
mockExtractEmbeddedImageIds.mockReturnValue(['a'])
122+
mockDownloadFile.mockResolvedValue(Buffer.alloc(250 * MB))
123+
124+
const response = await GET(request(), context)
125+
126+
expect(response.status).toBe(400)
127+
expect((await response.json()).error).toContain('document and its embedded files')
128+
})
129+
130+
it('caps the document body read rather than loading it unbounded', async () => {
131+
mockExtractEmbeddedImageIds.mockReturnValue([])
132+
133+
await GET(request(), context)
134+
135+
const bodyCall = mockDownloadFile.mock.calls.find(([options]) => options.key.endsWith('doc.md'))
136+
expect(bodyCall?.[0].maxBytes).toBe(250 * MB)
137+
})
138+
119139
it('caps each asset download rather than trusting its declared size', async () => {
120140
mockExtractEmbeddedImageIds.mockReturnValue(['a'])
121141

apps/sim/app/api/files/export/[id]/route.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,13 @@ export const GET = withRouteHandler(
128128
return NextResponse.redirect(new URL(servePath, request.url), { status: 302 })
129129
}
130130

131+
// Capped like everything else in the bundle: the document body is usually the
132+
// largest single entry, so leaving it unbounded left the export limit unenforced
133+
// against the one item most able to exceed it.
131134
const mdBuffer = await downloadFile({
132135
key: record.key,
133136
context: record.context as StorageContext,
137+
maxBytes: MAX_EXPORT_TOTAL_BYTES,
134138
})
135139
let mdContent = mdBuffer.toString('utf-8')
136140

@@ -180,11 +184,14 @@ export const GET = withRouteHandler(
180184
})
181185
).filter((target): target is NonNullable<typeof target> => target !== null)
182186

183-
const declaredAssetBytes = assetTargets.reduce((sum, target) => sum + target.record.size, 0)
184-
if (declaredAssetBytes > MAX_EXPORT_TOTAL_BYTES) {
187+
// The body counts against the same budget as its assets — the zip holds both, so a
188+
// limit that measured only the attachments would not describe the archive produced.
189+
const bundleBytes =
190+
mdBuffer.length + assetTargets.reduce((sum, target) => sum + target.record.size, 0)
191+
if (bundleBytes > MAX_EXPORT_TOTAL_BYTES) {
185192
return NextResponse.json(
186193
{
187-
error: `Embedded files total ${formatFileSize(declaredAssetBytes)}, which exceeds the ${formatFileSize(MAX_EXPORT_TOTAL_BYTES)} export limit.`,
194+
error: `This document and its embedded files total ${formatFileSize(bundleBytes)}, which exceeds the ${formatFileSize(MAX_EXPORT_TOTAL_BYTES)} export limit.`,
188195
},
189196
{ status: 400 }
190197
)

0 commit comments

Comments
 (0)