Skip to content

Commit 8142208

Browse files
authored
fix(mistral): distinguish response size failures (#7154)
1 parent 752c21c commit 8142208

2 files changed

Lines changed: 172 additions & 26 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import {
5+
createMockRequest,
6+
hybridAuthMockFns,
7+
inputValidationMock,
8+
inputValidationMockFns,
9+
} from '@sim/testing'
10+
import { beforeEach, describe, expect, it, vi } from 'vitest'
11+
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
12+
import { PRIVATE_MODEL_INPUT_PROVENANCE_HEADER } from '@/lib/execution/model-input-provenance'
13+
import {
14+
RESOLVED_SECRET_PROVENANCE_FIELD,
15+
RESOLVED_SECRET_PROVENANCE_METADATA_V1,
16+
} from '@/lib/execution/private-tool-metadata'
17+
import { MISTRAL_OCR_REQUEST_POLICY } from '@/lib/knowledge/documents/ocr-request-policy'
18+
19+
const { mockDownloadServableFile, mockIsModelSafeWorkspaceFileKey } = vi.hoisted(() => ({
20+
mockDownloadServableFile: vi.fn(),
21+
mockIsModelSafeWorkspaceFileKey: vi.fn(),
22+
}))
23+
24+
vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)
25+
vi.mock('@/app/api/files/authorization', () => ({
26+
assertToolFileAccess: vi.fn().mockResolvedValue(null),
27+
}))
28+
vi.mock('@/lib/uploads/utils/file-utils.server', () => ({
29+
downloadServableFileFromStorage: mockDownloadServableFile,
30+
resolveInternalFileUrl: vi.fn(),
31+
}))
32+
vi.mock('@/lib/uploads/contexts/workspace/workspace-file-secret-provenance', () => ({
33+
isModelSafeWorkspaceFileKey: mockIsModelSafeWorkspaceFileKey,
34+
MODEL_UNSAFE_WORKSPACE_FILE_ERROR_MESSAGE:
35+
'File cannot be sent to a model because its secret provenance is unavailable',
36+
}))
37+
38+
import { POST } from '@/app/api/tools/mistral/parse/route'
39+
40+
const PDF_FILE = {
41+
key: 'workspace/workspace-1/document.pdf',
42+
name: 'document.pdf',
43+
size: 3,
44+
type: 'application/pdf',
45+
}
46+
47+
function createVerifiedRequest() {
48+
return createMockRequest(
49+
'POST',
50+
{
51+
apiKey: 'mistral-key',
52+
file: PDF_FILE,
53+
[RESOLVED_SECRET_PROVENANCE_FIELD]: {
54+
version: 1,
55+
complete: true,
56+
entries: [],
57+
},
58+
},
59+
{ [PRIVATE_MODEL_INPUT_PROVENANCE_HEADER]: RESOLVED_SECRET_PROVENANCE_METADATA_V1 }
60+
)
61+
}
62+
63+
describe('POST /api/tools/mistral/parse', () => {
64+
beforeEach(() => {
65+
vi.clearAllMocks()
66+
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
67+
success: true,
68+
userId: 'user-1',
69+
authType: 'internal_jwt',
70+
})
71+
inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValue({
72+
isValid: true,
73+
resolvedIP: '93.184.216.34',
74+
originalHostname: 'api.mistral.ai',
75+
})
76+
mockIsModelSafeWorkspaceFileKey.mockResolvedValue(true)
77+
mockDownloadServableFile.mockResolvedValue({
78+
buffer: Buffer.from('pdf'),
79+
contentType: 'application/pdf',
80+
})
81+
inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValue(
82+
Response.json({ pages: [], usage_info: { pages_processed: 0 } })
83+
)
84+
})
85+
86+
it('returns 413 when the input file exceeds Mistral request limits', async () => {
87+
mockDownloadServableFile.mockRejectedValueOnce(
88+
new PayloadSizeLimitError({
89+
label: 'storage file download',
90+
maxBytes: MISTRAL_OCR_REQUEST_POLICY.maxBytes,
91+
observedBytes: MISTRAL_OCR_REQUEST_POLICY.maxBytes + 1,
92+
})
93+
)
94+
95+
const response = await POST(createVerifiedRequest())
96+
97+
expect(response.status).toBe(413)
98+
await expect(response.json()).resolves.toEqual({
99+
success: false,
100+
error: `File exceeds Mistral OCR's ${MISTRAL_OCR_REQUEST_POLICY.maxBytes.toLocaleString()}-byte request limit`,
101+
})
102+
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
103+
})
104+
105+
it('returns 502 when Mistral response bytes exceed the secure-fetch cap', async () => {
106+
const responseLimitError = new PayloadSizeLimitError({
107+
label: 'response body',
108+
maxBytes: 100,
109+
observedBytes: 101,
110+
})
111+
inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce({
112+
ok: true,
113+
status: 200,
114+
statusText: 'OK',
115+
headers: new Headers(),
116+
body: null,
117+
text: async () => {
118+
throw responseLimitError
119+
},
120+
json: async () => {
121+
throw responseLimitError
122+
},
123+
arrayBuffer: async () => {
124+
throw responseLimitError
125+
},
126+
})
127+
128+
const response = await POST(createVerifiedRequest())
129+
130+
expect(response.status).toBe(502)
131+
await expect(response.json()).resolves.toEqual({
132+
success: false,
133+
error: 'Mistral API response exceeded the safe size limit',
134+
})
135+
})
136+
})

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

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
import {
2929
downloadServableFileFromStorage,
3030
resolveInternalFileUrl,
31+
type ServableFile,
3132
} from '@/lib/uploads/utils/file-utils.server'
3233
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
3334
import { assertToolFileAccess } from '@/app/api/files/authorization'
@@ -36,6 +37,16 @@ export const dynamic = 'force-dynamic'
3637

3738
const logger = createLogger('MistralParseAPI')
3839

40+
function fileSizeLimitResponse() {
41+
return NextResponse.json(
42+
{
43+
success: false,
44+
error: `File exceeds Mistral OCR's ${MISTRAL_OCR_REQUEST_POLICY.maxBytes.toLocaleString()}-byte request limit`,
45+
},
46+
{ status: 413 }
47+
)
48+
}
49+
3950
export const POST = withRouteHandler(async (request: NextRequest) => {
4051
const requestId = generateRequestId()
4152

@@ -159,14 +170,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
159170
{ status: 400 }
160171
)
161172
}
162-
const { buffer, contentType } = await downloadServableFileFromStorage(
163-
userFile,
164-
requestId,
165-
logger,
166-
{
173+
let servableFile: ServableFile
174+
try {
175+
servableFile = await downloadServableFileFromStorage(userFile, requestId, logger, {
167176
maxBytes: MISTRAL_OCR_REQUEST_POLICY.maxBytes,
168-
}
169-
)
177+
})
178+
} catch (error) {
179+
if (!isPayloadSizeLimitError(error)) throw error
180+
return fileSizeLimitResponse()
181+
}
182+
const { buffer, contentType } = servableFile
170183
base64 = buffer.toString('base64')
171184
if (contentType && contentType !== 'application/octet-stream') {
172185
mimeType = contentType
@@ -180,25 +193,18 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
180193
: Buffer.byteLength(base64, 'base64')
181194
} catch (error) {
182195
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-
)
196+
return status === 413
197+
? fileSizeLimitResponse()
198+
: NextResponse.json(
199+
{
200+
success: false,
201+
error: getErrorMessage(error, 'Invalid inline file data'),
202+
},
203+
{ status }
204+
)
193205
}
194206
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-
)
207+
return fileSizeLimitResponse()
202208
}
203209

204210
const base64Payload = base64.startsWith('data:')
@@ -355,12 +361,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
355361
if (notReady) return notReady
356362

357363
if (isPayloadSizeLimitError(error)) {
364+
logger.error(`[${requestId}] Mistral API response exceeded the safe size limit`, {
365+
maxBytes: error.maxBytes,
366+
observedBytes: error.observedBytes,
367+
})
358368
return NextResponse.json(
359369
{
360370
success: false,
361-
error: `File exceeds Mistral OCR's ${MISTRAL_OCR_REQUEST_POLICY.maxBytes.toLocaleString()}-byte request limit`,
371+
error: 'Mistral API response exceeded the safe size limit',
362372
},
363-
{ status: 413 }
373+
{ status: 502 }
364374
)
365375
}
366376

0 commit comments

Comments
 (0)