Skip to content

Commit 1afaf46

Browse files
authored
fix(knowledge): harden OCR and SharePoint ingestion limits (#7135)
* fix(connectors): fail closed on incomplete SharePoint traversal * fix(knowledge): enforce OCR request limits by bytes and pages * fix(knowledge): retain OCR policy literals
1 parent 562d918 commit 1afaf46

9 files changed

Lines changed: 525 additions & 119 deletions

File tree

apps/sim/app/api/tools/mistral/parse/route.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ import {
99
validateUrlWithDNS,
1010
} from '@/lib/core/security/input-validation.server'
1111
import { generateRequestId } from '@/lib/core/utils/request'
12+
import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
1213
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1314
import { validateOpaqueModelInputProvenance } from '@/lib/execution/model-input-provenance'
15+
import { decodeDataUriWithinLimit } from '@/lib/file-parsers/data-uri'
16+
import { isFileParserError } from '@/lib/file-parsers/errors'
17+
import { MISTRAL_OCR_REQUEST_POLICY } from '@/lib/knowledge/documents/ocr-request-policy'
18+
import { readBoundedHttpErrorBody } from '@/lib/knowledge/documents/utils'
1419
import {
1520
isModelSafeWorkspaceFileKey,
1621
MODEL_UNSAFE_WORKSPACE_FILE_ERROR_MESSAGE,
1722
} from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance'
18-
import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types'
1923
import {
2024
extractStorageKey,
2125
isInternalFileUrl,
@@ -160,14 +164,43 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
160164
requestId,
161165
logger,
162166
{
163-
maxBytes: MAX_BUFFERED_TRANSFER_BYTES,
167+
maxBytes: MISTRAL_OCR_REQUEST_POLICY.maxBytes,
164168
}
165169
)
166170
base64 = buffer.toString('base64')
167171
if (contentType && contentType !== 'application/octet-stream') {
168172
mimeType = contentType
169173
}
170174
}
175+
176+
let inlineBytes: number
177+
try {
178+
inlineBytes = base64.startsWith('data:')
179+
? decodeDataUriWithinLimit(base64, MISTRAL_OCR_REQUEST_POLICY.maxBytes).buffer.length
180+
: Buffer.byteLength(base64, 'base64')
181+
} catch (error) {
182+
const status = isFileParserError(error) && error.code === 'complexity_limit' ? 413 : 400
183+
return NextResponse.json(
184+
{
185+
success: false,
186+
error:
187+
status === 413
188+
? `File exceeds Mistral OCR's ${MISTRAL_OCR_REQUEST_POLICY.maxBytes.toLocaleString()}-byte request limit`
189+
: getErrorMessage(error, 'Invalid inline file data'),
190+
},
191+
{ status }
192+
)
193+
}
194+
if (inlineBytes > MISTRAL_OCR_REQUEST_POLICY.maxBytes) {
195+
return NextResponse.json(
196+
{
197+
success: false,
198+
error: `File exceeds Mistral OCR's ${MISTRAL_OCR_REQUEST_POLICY.maxBytes.toLocaleString()}-byte request limit`,
199+
},
200+
{ status: 413 }
201+
)
202+
}
203+
171204
const base64Payload = base64.startsWith('data:')
172205
? base64
173206
: `data:${mimeType};base64,${base64}`
@@ -295,8 +328,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
295328
)
296329

297330
if (!mistralResponse.ok) {
298-
const errorText = await mistralResponse.text()
299-
logger.error(`[${requestId}] Mistral API error:`, errorText)
331+
const errorText = await readBoundedHttpErrorBody(mistralResponse)
332+
logger.error(`[${requestId}] Mistral API error`, {
333+
status: mistralResponse.status,
334+
diagnostic: errorText,
335+
})
300336
return NextResponse.json(
301337
{
302338
success: false,
@@ -318,6 +354,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
318354
const notReady = docNotReadyResponse(error)
319355
if (notReady) return notReady
320356

357+
if (isPayloadSizeLimitError(error)) {
358+
return NextResponse.json(
359+
{
360+
success: false,
361+
error: `File exceeds Mistral OCR's ${MISTRAL_OCR_REQUEST_POLICY.maxBytes.toLocaleString()}-byte request limit`,
362+
},
363+
{ status: 413 }
364+
)
365+
}
366+
321367
logger.error(`[${requestId}] Error in Mistral parse:`, error)
322368

323369
return NextResponse.json(

apps/sim/connectors/sharepoint/sharepoint.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,29 @@ function rootChildren(driveId: string, items: unknown[]) {
105105
}
106106
}
107107

108+
/** Builds a Graph pagination chain with an optional continuation beyond the final allowed page. */
109+
function paginatedRoutes(
110+
initialUrl: string,
111+
routePrefix: string,
112+
pageCount: number,
113+
continueAfterLast: boolean
114+
): Record<string, GraphRoute> {
115+
const routes: Record<string, GraphRoute> = {}
116+
117+
for (let page = 0; page < pageCount; page++) {
118+
const url = page === 0 ? initialUrl : `${GRAPH}/${routePrefix}/${page}`
119+
const hasNextPage = page < pageCount - 1 || continueAfterLast
120+
routes[url] = {
121+
body: {
122+
value: [],
123+
...(hasNextPage ? { '@odata.nextLink': `${GRAPH}/${routePrefix}/${page + 1}` } : {}),
124+
},
125+
}
126+
}
127+
128+
return routes
129+
}
130+
108131
function resolve(folderPath?: string) {
109132
return resolveFolderTarget('token', SITE_ID, SITE_URL, 'Contoso', folderPath)
110133
}
@@ -322,6 +345,51 @@ describe('resolveFolderTarget', () => {
322345
)
323346
})
324347

348+
it('surfaces a terminal document-library listing failure', async () => {
349+
mockGraph({
350+
...defaultDriveRoute,
351+
[`${GRAPH}/sites/${SITE_ID}/drives?$select=id,name,webUrl`]: {
352+
status: 503,
353+
body: { error: { message: 'Service unavailable' } },
354+
},
355+
})
356+
357+
await expect(resolve('Missing')).rejects.toThrow(
358+
/Failed to list SharePoint document libraries: 503/
359+
)
360+
})
361+
362+
it('rejects a document-library listing that continues beyond its safety limit', async () => {
363+
const initialUrl = `${GRAPH}/sites/${SITE_ID}/drives?$select=id,name,webUrl`
364+
mockGraph({
365+
...defaultDriveRoute,
366+
...paginatedRoutes(initialUrl, 'drive-pages', 20, true),
367+
})
368+
369+
await expect(resolve('Missing')).rejects.toThrow(
370+
/document-library listing exceeded the 20-page safety limit/
371+
)
372+
})
373+
374+
it('surfaces a 404 encountered while traversing a folder collection', async () => {
375+
mockGraph({ ...defaultDriveRoute, ...sitesDrivesRoute })
376+
377+
await expect(resolve('Missing')).rejects.toThrow(/Failed to list folder contents: 404/)
378+
})
379+
380+
it('rejects a folder listing that continues beyond its safety limit', async () => {
381+
const initialUrl = `${GRAPH}/drives/${DEFAULT_DRIVE_ID}/root/children?$top=200&$select=id,name,folder`
382+
mockGraph({
383+
...defaultDriveRoute,
384+
...sitesDrivesRoute,
385+
...paginatedRoutes(initialUrl, 'folder-pages', 50, true),
386+
})
387+
388+
await expect(resolve('Missing')).rejects.toThrow(
389+
/folder listing exceeded the 50-page safety limit/
390+
)
391+
})
392+
325393
it('accepts an address-bar folder URL carrying the path in the id parameter', async () => {
326394
mockGraph({
327395
...defaultDriveRoute,

apps/sim/connectors/sharepoint/sharepoint.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -425,9 +425,9 @@ async function listChildFolders(
425425

426426
for (let page = 0; page < MAX_CHILD_PAGES_PER_SEGMENT; page++) {
427427
const response = await graphGet(url, accessToken, retryOptions)
428-
if (response.status === 404) break
429428
if (!response.ok) {
430-
throw new Error(`Failed to list folder contents: ${response.status}`)
429+
const errorText = await readBoundedHttpErrorBody(response)
430+
throw new Error(`Failed to list folder contents: ${response.status}${errorText}`)
431431
}
432432

433433
const rawData: unknown = await response.json()
@@ -455,11 +455,16 @@ async function listChildFolders(
455455
rawData['@odata.nextLink'] === undefined
456456
? undefined
457457
: assertMicrosoftGraphNextLink(rawData['@odata.nextLink'])
458-
if (!nextLink) break
458+
if (!nextLink) return folders
459+
if (page === MAX_CHILD_PAGES_PER_SEGMENT - 1) {
460+
throw new Error(
461+
`SharePoint folder listing exceeded the ${MAX_CHILD_PAGES_PER_SEGMENT}-page safety limit`
462+
)
463+
}
459464
url = nextLink
460465
}
461466

462-
return folders
467+
throw new Error('SharePoint folder listing ended unexpectedly')
463468
}
464469

465470
/**
@@ -668,15 +673,25 @@ async function listSiteDrives(
668673

669674
for (let page = 0; page < MAX_DRIVE_PAGES; page++) {
670675
const response = await graphGet(url, accessToken, retryOptions)
671-
if (!response.ok) break
676+
if (!response.ok) {
677+
const errorText = await readBoundedHttpErrorBody(response)
678+
throw new Error(
679+
`Failed to list SharePoint document libraries: ${response.status}${errorText}`
680+
)
681+
}
672682
const data = parseDriveListResponse(await response.json())
673683
drives.push(...data.value)
674684
const nextLink = data['@odata.nextLink']
675-
if (!nextLink) break
685+
if (!nextLink) return drives
686+
if (page === MAX_DRIVE_PAGES - 1) {
687+
throw new Error(
688+
`SharePoint document-library listing exceeded the ${MAX_DRIVE_PAGES}-page safety limit`
689+
)
690+
}
676691
url = nextLink
677692
}
678693

679-
return drives
694+
throw new Error('SharePoint document-library listing ended unexpectedly')
680695
}
681696

682697
/**

0 commit comments

Comments
 (0)