@@ -12,6 +12,7 @@ import { TraceEvent } from '@/lib/copilot/generated/trace-events-v1'
1212import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1'
1313import { recordFileRead } from '@/lib/copilot/request/metrics'
1414import { markSpanForError } from '@/lib/copilot/request/otel'
15+ import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
1516import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
1617import { fetchWorkspaceFileBuffer } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
1718import { isHeifContainer , transcodeHeicToJpeg } from '@/lib/uploads/server/heic'
@@ -79,6 +80,29 @@ function getExtension(filename: string): string {
7980 return dot >= 0 ? filename . slice ( dot + 1 ) . toLowerCase ( ) : ''
8081}
8182
83+ /**
84+ * Download a record under an authoritative byte cap, returning null when the stored
85+ * object breaches it. `record.size` is client-declared, so a caller's own size check
86+ * can pass while the real bytes do not — this is the check that actually holds, and
87+ * null lets the caller answer with its too-large placeholder rather than a read failure.
88+ */
89+ async function fetchWithinLimit (
90+ record : WorkspaceFileRecord ,
91+ maxBytes : number
92+ ) : Promise < Buffer | null > {
93+ try {
94+ return await fetchWorkspaceFileBuffer ( record , { maxBytes } )
95+ } catch ( err ) {
96+ if ( ! isPayloadSizeLimitError ( err ) ) throw err
97+ logger . warn ( 'Workspace file exceeded its read cap' , {
98+ fileName : record . name ,
99+ recordedSize : record . size ,
100+ maxBytes,
101+ } )
102+ return null
103+ }
104+ }
105+
82106function detectImageMime ( buf : Buffer , claimed : string ) : string {
83107 if ( buf . length < 12 ) return claimed
84108 if ( buf [ 0 ] === 0xff && buf [ 1 ] === 0xd8 && buf [ 2 ] === 0xff ) return 'image/jpeg'
@@ -404,18 +428,22 @@ export async function readFileRecord(record: WorkspaceFileRecord): Promise<FileR
404428 // image down the binary path where the model never sees it.
405429 if ( isImageFileType ( resolveEffectiveMimeType ( record . type , record . name ) ) ) {
406430 span . setAttribute ( TraceAttr . CopilotVfsReadPath , CopilotVfsReadPath . Image )
407- // `record.size` is client-declared, so it only buys the friendly placeholder;
408- // the download's own `maxBytes` is what actually bounds the bytes read.
431+ // `record.size` is client-declared, so it only skips a doomed download; the
432+ // download's own `maxBytes` is what actually bounds the bytes read. Both
433+ // answer with the same placeholder.
434+ const imageTooLarge = {
435+ content : `[Image too large to read inline: ${ record . name } (${ record . size } bytes, limit ${ MAX_IMAGE_SOURCE_BYTES } )]` ,
436+ totalLines : 1 ,
437+ }
409438 if ( record . size > MAX_IMAGE_SOURCE_BYTES ) {
410439 span . setAttribute ( TraceAttr . CopilotVfsReadOutcome , CopilotVfsReadOutcome . ImageTooLarge )
411- return {
412- content : `[Image too large to read inline: ${ record . name } (${ record . size } bytes, limit ${ MAX_IMAGE_SOURCE_BYTES } )]` ,
413- totalLines : 1 ,
414- }
440+ return imageTooLarge
441+ }
442+ const originalBuffer = await fetchWithinLimit ( record , MAX_IMAGE_SOURCE_BYTES )
443+ if ( ! originalBuffer ) {
444+ span . setAttribute ( TraceAttr . CopilotVfsReadOutcome , CopilotVfsReadOutcome . ImageTooLarge )
445+ return imageTooLarge
415446 }
416- const originalBuffer = await fetchWorkspaceFileBuffer ( record , {
417- maxBytes : MAX_IMAGE_SOURCE_BYTES ,
418- } )
419447 const prepared = await prepareImageForVision ( originalBuffer , record . type )
420448 if ( ! prepared . ok ) {
421449 span . setAttribute ( TraceAttr . CopilotVfsReadOutcome , CopilotVfsReadOutcome . ImageTooLarge )
@@ -450,15 +478,20 @@ export async function readFileRecord(record: WorkspaceFileRecord): Promise<FileR
450478
451479 if ( isReadableFileType ( record . type ) ) {
452480 span . setAttribute ( TraceAttr . CopilotVfsReadPath , CopilotVfsReadPath . Text )
481+ const textTooLarge = {
482+ content : `[File too large to display inline: ${ record . name } (${ record . size } bytes, limit ${ MAX_TEXT_READ_BYTES } )]` ,
483+ totalLines : 1 ,
484+ }
453485 if ( record . size > MAX_TEXT_READ_BYTES ) {
454486 span . setAttribute ( TraceAttr . CopilotVfsReadOutcome , CopilotVfsReadOutcome . TextTooLarge )
455- return {
456- content : `[File too large to display inline: ${ record . name } (${ record . size } bytes, limit ${ MAX_TEXT_READ_BYTES } )]` ,
457- totalLines : 1 ,
458- }
487+ return textTooLarge
459488 }
460489
461- const buffer = await fetchWorkspaceFileBuffer ( record , { maxBytes : MAX_TEXT_READ_BYTES } )
490+ const buffer = await fetchWithinLimit ( record , MAX_TEXT_READ_BYTES )
491+ if ( ! buffer ) {
492+ span . setAttribute ( TraceAttr . CopilotVfsReadOutcome , CopilotVfsReadOutcome . TextTooLarge )
493+ return textTooLarge
494+ }
462495 const content = buffer . toString ( 'utf-8' )
463496 const lines = content . split ( '\n' ) . length
464497 span . setAttributes ( {
@@ -472,19 +505,25 @@ export async function readFileRecord(record: WorkspaceFileRecord): Promise<FileR
472505 const ext = getExtension ( record . name )
473506 if ( PARSEABLE_EXTENSIONS . has ( ext ) ) {
474507 span . setAttribute ( TraceAttr . CopilotVfsReadPath , CopilotVfsReadPath . ParseableDocument )
508+ const documentTooLarge = {
509+ content : `[Document too large to parse inline: ${ record . name } (${ record . size } bytes, limit ${ MAX_PARSEABLE_READ_BYTES } )]` ,
510+ totalLines : 1 ,
511+ }
475512 if ( record . size > MAX_PARSEABLE_READ_BYTES ) {
476513 span . setAttribute (
477514 TraceAttr . CopilotVfsReadOutcome ,
478515 CopilotVfsReadOutcome . DocumentTooLarge
479516 )
480- return {
481- content : `[Document too large to parse inline: ${ record . name } (${ record . size } bytes, limit ${ MAX_PARSEABLE_READ_BYTES } )]` ,
482- totalLines : 1 ,
483- }
517+ return documentTooLarge
518+ }
519+ const buffer = await fetchWithinLimit ( record , MAX_PARSEABLE_READ_BYTES )
520+ if ( ! buffer ) {
521+ span . setAttribute (
522+ TraceAttr . CopilotVfsReadOutcome ,
523+ CopilotVfsReadOutcome . DocumentTooLarge
524+ )
525+ return documentTooLarge
484526 }
485- const buffer = await fetchWorkspaceFileBuffer ( record , {
486- maxBytes : MAX_PARSEABLE_READ_BYTES ,
487- } )
488527 try {
489528 const { parseBuffer } = await import ( '@/lib/file-parsers' )
490529 const result = await parseBuffer ( buffer , ext )
0 commit comments