Skip to content

Commit e8e801e

Browse files
sallyomclaude
andcommitted
Add automatic image compression for file uploads to prevent SDK buffer overflow
- Reduced MAX_IMAGE_SIZE from 1MB to 500KB to account for base64 encoding overhead - Added compressImageIfNeeded() function using sharp library for server-side image processing - Implements multi-stage compression: quality reduction (80% → 20%), then dimension scaling - Automatically compresses JPEG/PNG/WebP images exceeding 500KB before upload - Prevents Claude SDK JSON buffer overflow (1MB limit) when reading large screenshots - Added sharp@^0.33.0 dependency to frontend package.json - Graceful error handling if compression fails Fixes the "JSON message exceeded maximum buffer size" error for large image uploads. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 364af1c commit e8e801e

File tree

2 files changed

+133
-28
lines changed
  • components/frontend

2 files changed

+133
-28
lines changed

components/frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"react-resizable-panels": "^3.0.6",
3838
"rehype-highlight": "^7.0.2",
3939
"remark-gfm": "^4.0.1",
40+
"sharp": "^0.33.0",
4041
"tailwind-merge": "^3.3.1",
4142
"zod": "^4.1.5"
4243
},

components/frontend/src/app/api/projects/[name]/agentic-sessions/[sessionName]/workspace/upload/route.ts

Lines changed: 132 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { NextRequest } from 'next/server';
44

55
// Maximum file sizes based on type
66
const MAX_DOCUMENT_SIZE = 8 * 1024 * 1024; // 8MB for documents
7-
const MAX_IMAGE_SIZE = 1 * 1024 * 1024; // 1MB for images
7+
const MAX_IMAGE_SIZE = 500 * 1024; // 500KB for images (SDK has 1MB JSON limit, base64 adds ~33% overhead)
88

99
// Determine if a file is an image based on content type
1010
const isImageFile = (contentType: string): boolean => {
@@ -23,6 +23,75 @@ const formatSizeLimit = (contentType: string): string => {
2323
return `${sizeInMB}MB for ${fileType}`;
2424
};
2525

26+
// Compress image if it exceeds size limit
27+
// Returns compressed image buffer or original if already small enough
28+
async function compressImageIfNeeded(
29+
buffer: ArrayBuffer,
30+
contentType: string,
31+
maxSize: number
32+
): Promise<ArrayBuffer> {
33+
// Only compress actual images (not SVGs or other formats that won't benefit)
34+
if (!contentType.match(/^image\/(jpeg|jpg|png|webp)$/i)) {
35+
return buffer;
36+
}
37+
38+
// If already under limit, return as-is
39+
if (buffer.byteLength <= maxSize) {
40+
return buffer;
41+
}
42+
43+
// Use canvas API to resize and compress
44+
// Note: This requires browser APIs, but we're in Next.js API route (Node.js)
45+
// We'll use sharp library for server-side image processing
46+
try {
47+
const sharp = (await import('sharp')).default;
48+
49+
// Start with moderate compression
50+
let quality = 80;
51+
let compressed = await sharp(Buffer.from(buffer))
52+
.jpeg({ quality, mozjpeg: true }) // Convert to JPEG for better compression
53+
.toBuffer();
54+
55+
// Reduce quality iteratively if still too large
56+
while (compressed.byteLength > maxSize && quality > 20) {
57+
quality -= 10;
58+
compressed = await sharp(Buffer.from(buffer))
59+
.jpeg({ quality, mozjpeg: true })
60+
.toBuffer();
61+
}
62+
63+
// If still too large, resize dimensions
64+
if (compressed.byteLength > maxSize) {
65+
const metadata = await sharp(Buffer.from(buffer)).metadata();
66+
const width = metadata.width || 1920;
67+
const height = metadata.height || 1080;
68+
69+
// Reduce by 25% iteratively
70+
let scale = 0.75;
71+
while (compressed.byteLength > maxSize && scale > 0.25) {
72+
compressed = await sharp(Buffer.from(buffer))
73+
.resize(Math.floor(width * scale), Math.floor(height * scale), {
74+
fit: 'inside',
75+
withoutEnlargement: true,
76+
})
77+
.jpeg({ quality: 70, mozjpeg: true })
78+
.toBuffer();
79+
scale -= 0.1;
80+
}
81+
}
82+
83+
console.log(
84+
`Compressed image: ${buffer.byteLength} bytes -> ${compressed.byteLength} bytes (${Math.round((compressed.byteLength / buffer.byteLength) * 100)}%)`
85+
);
86+
87+
return compressed.buffer;
88+
} catch (error) {
89+
console.error('Failed to compress image:', error);
90+
// If compression fails, throw error rather than uploading oversized file
91+
throw new Error('Image too large and compression failed');
92+
}
93+
}
94+
2695
export async function POST(
2796
request: NextRequest,
2897
{ params }: { params: Promise<{ name: string; sessionName: string }> },
@@ -46,22 +115,40 @@ export async function POST(
46115

47116
const filename = (formData.get('filename') as string) || file.name;
48117
const contentType = file.type || 'application/octet-stream';
49-
50-
// Check file size based on type
51118
const maxSize = getMaxFileSize(contentType);
52-
if (file.size > maxSize) {
53-
return new Response(
54-
JSON.stringify({
55-
error: `File too large. Maximum size is ${formatSizeLimit(contentType)}`
56-
}),
57-
{
58-
status: 413, // Payload Too Large
59-
headers: { 'Content-Type': 'application/json' },
60-
}
61-
);
62-
}
63119

64-
const fileBuffer = await file.arrayBuffer();
120+
// Get initial file buffer
121+
let fileBuffer = await file.arrayBuffer();
122+
123+
// For images, compress if needed instead of rejecting
124+
if (isImageFile(contentType)) {
125+
try {
126+
fileBuffer = await compressImageIfNeeded(fileBuffer, contentType, maxSize);
127+
} catch (compressionError) {
128+
return new Response(
129+
JSON.stringify({
130+
error: `Image too large and could not be compressed. Please reduce image size and try again.`
131+
}),
132+
{
133+
status: 413, // Payload Too Large
134+
headers: { 'Content-Type': 'application/json' },
135+
}
136+
);
137+
}
138+
} else {
139+
// For non-images, enforce strict size limit
140+
if (file.size > maxSize) {
141+
return new Response(
142+
JSON.stringify({
143+
error: `File too large. Maximum size is ${formatSizeLimit(contentType)}`
144+
}),
145+
{
146+
status: 413, // Payload Too Large
147+
headers: { 'Content-Type': 'application/json' },
148+
}
149+
);
150+
}
151+
}
65152

66153
// Upload to workspace/file-uploads directory using the PUT endpoint
67154
// Retry logic: if backend returns 202 (content service starting), retry up to 3 times
@@ -133,21 +220,38 @@ export async function POST(
133220
});
134221
}
135222

136-
const fileBuffer = await fileResp.arrayBuffer();
223+
let fileBuffer = await fileResp.arrayBuffer();
137224
const contentType = fileResp.headers.get('content-type') || 'application/octet-stream';
138-
139-
// Check file size based on type
140225
const maxSize = getMaxFileSize(contentType);
141-
if (fileBuffer.byteLength > maxSize) {
142-
return new Response(
143-
JSON.stringify({
144-
error: `File too large. Maximum size is ${formatSizeLimit(contentType)}`
145-
}),
146-
{
147-
status: 413, // Payload Too Large
148-
headers: { 'Content-Type': 'application/json' },
149-
}
150-
);
226+
227+
// For images, compress if needed instead of rejecting
228+
if (isImageFile(contentType)) {
229+
try {
230+
fileBuffer = await compressImageIfNeeded(fileBuffer, contentType, maxSize);
231+
} catch (compressionError) {
232+
return new Response(
233+
JSON.stringify({
234+
error: `Image too large and could not be compressed. Please reduce image size and try again.`
235+
}),
236+
{
237+
status: 413, // Payload Too Large
238+
headers: { 'Content-Type': 'application/json' },
239+
}
240+
);
241+
}
242+
} else {
243+
// For non-images, enforce strict size limit
244+
if (fileBuffer.byteLength > maxSize) {
245+
return new Response(
246+
JSON.stringify({
247+
error: `File too large. Maximum size is ${formatSizeLimit(contentType)}`
248+
}),
249+
{
250+
status: 413, // Payload Too Large
251+
headers: { 'Content-Type': 'application/json' },
252+
}
253+
);
254+
}
151255
}
152256

153257
// Upload to workspace/file-uploads directory using the PUT endpoint

0 commit comments

Comments
 (0)