fix(upload): increase attachment size limit to 50mb (#144)
* fix(upload): increase attachment size limit to 50mb Raises body limits in web proxy and desktop app. Adds client-side size checks and better 413 error handling. * fix(client): auto-compress large images to avoid server body limits Adds client-side compression for images >1MB. Resizes to max 2048px and converts large PNGs to JPEG. Adds detailed logging for debugging. * chore: remove debug logs from client and proxy
This commit is contained in:
committed by
GitHub
parent
5ffec2acba
commit
c4fb9d5730
@@ -84,7 +84,7 @@ use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
|
||||
#[cfg(target_os = "macos")]
|
||||
static NEEDS_TRAFFIC_LIGHT_FIX: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
const PROXY_BODY_LIMIT: usize = 32 * 1024 * 1024; // 32MB
|
||||
const PROXY_BODY_LIMIT: usize = 50 * 1024 * 1024; // 50MB
|
||||
const CLIENT_RELOAD_DELAY_MS: u64 = 800;
|
||||
const MODELS_DEV_API_URL: &str = "https://models.dev/api.json";
|
||||
const MODELS_METADATA_CACHE_TTL: Duration = Duration::from_secs(5 * 60);
|
||||
|
||||
@@ -519,6 +519,14 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
normalized.includes('gateway timeout') ||
|
||||
normalized === 'failed to send message';
|
||||
|
||||
if (normalized.includes('payload too large') || normalized.includes('413') || normalized.includes('entity too large')) {
|
||||
toast.error('Attachments are too large to send. Please try reducing the number or size of images.');
|
||||
if (allAttachments.length > 0) {
|
||||
useFileStore.setState({ attachedFiles: allAttachments });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSoftNetworkError) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -432,6 +432,56 @@ class OpencodeService {
|
||||
return lowerMime === 'image/heic' || lowerMime === 'image/heif';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress and resize image using Canvas.
|
||||
*/
|
||||
private async compressImage(dataUrl: string, mimeType: string, quality = 0.8, maxWidth = 2048): Promise<string> {
|
||||
if (typeof document === 'undefined') return dataUrl;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
if (width > maxWidth || height > maxWidth) {
|
||||
if (width > height) {
|
||||
height = Math.round(height * (maxWidth / width));
|
||||
width = maxWidth;
|
||||
} else {
|
||||
width = Math.round(width * (maxWidth / height));
|
||||
height = maxWidth;
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
resolve(dataUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
let targetMime = mimeType;
|
||||
// Convert large PNGs to JPEG to save space
|
||||
if (mimeType === 'image/png' && dataUrl.length > 2.5 * 1024 * 1024) {
|
||||
targetMime = 'image/jpeg';
|
||||
}
|
||||
|
||||
try {
|
||||
resolve(canvas.toDataURL(targetMime, quality));
|
||||
} catch {
|
||||
resolve(dataUrl);
|
||||
}
|
||||
};
|
||||
img.onerror = () => resolve(dataUrl);
|
||||
img.src = dataUrl;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HEIC image to JPEG.
|
||||
* Returns the original file if conversion fails.
|
||||
@@ -496,6 +546,23 @@ class OpencodeService {
|
||||
return this.convertHeicToJpeg(file);
|
||||
}
|
||||
|
||||
// Handle large image compression (Resize > 2048px or > 1MB)
|
||||
if (file.mime.startsWith('image/') && (file.mime === 'image/jpeg' || file.mime === 'image/png' || file.mime === 'image/webp')) {
|
||||
// > ~1MB base64
|
||||
if (file.url.length > 1.33 * 1024 * 1024) {
|
||||
const compressedUrl = await this.compressImage(file.url, file.mime);
|
||||
const newMime = compressedUrl.startsWith('data:image/jpeg') ? 'image/jpeg' : file.mime;
|
||||
// Update the file object with compressed data
|
||||
// We return a new object to avoid mutating the original file ref if used elsewhere,
|
||||
// but here we just return the part.
|
||||
return {
|
||||
...file,
|
||||
mime: newMime,
|
||||
url: compressedUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle text MIME normalization
|
||||
if (!this.shouldNormalizeToTextPlain(file.mime)) {
|
||||
return file;
|
||||
|
||||
@@ -17,7 +17,7 @@ interface FileActions {
|
||||
|
||||
type FileStore = FileState & FileActions;
|
||||
|
||||
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
|
||||
const MAX_ATTACHMENT_SIZE = 50 * 1024 * 1024;
|
||||
|
||||
const guessMimeTypeFromName = (filename: string): string => {
|
||||
const name = (filename || "").toLowerCase();
|
||||
@@ -135,7 +135,7 @@ export const useFileStore = create<FileStore>()(
|
||||
|
||||
const maxSize = MAX_ATTACHMENT_SIZE;
|
||||
if (file.size > maxSize) {
|
||||
throw new Error(`File "${file.name}" is too large. Maximum size is 10MB.`);
|
||||
throw new Error(`File "${file.name}" is too large. Maximum size is 50MB.`);
|
||||
}
|
||||
|
||||
const allowedTypes = [
|
||||
@@ -251,7 +251,7 @@ export const useFileStore = create<FileStore>()(
|
||||
: new TextEncoder().encode(fileContent || "").length;
|
||||
|
||||
if (sizeBytes > MAX_ATTACHMENT_SIZE) {
|
||||
throw new Error(`File "${name}" is too large. Maximum size is 10MB.`);
|
||||
throw new Error(`File "${name}" is too large. Maximum size is 50MB.`);
|
||||
}
|
||||
|
||||
const file = new File([], name, { type: safeMimeType });
|
||||
|
||||
@@ -1851,6 +1851,7 @@ function setupProxy(app) {
|
||||
},
|
||||
onProxyReq: (proxyReq, req, res) => {
|
||||
console.log(`Proxying ${req.method} ${req.path} to OpenCode`);
|
||||
|
||||
if (req.headers.accept && req.headers.accept.includes('text/event-stream')) {
|
||||
console.log(`[SSE] Setting up SSE proxy for ${req.method} ${req.path}`);
|
||||
proxyReq.setHeader('Accept', 'text/event-stream');
|
||||
@@ -1997,16 +1998,16 @@ async function main(options = {}) {
|
||||
req.path.startsWith('/api/opencode')
|
||||
) {
|
||||
|
||||
express.json()(req, res, next);
|
||||
express.json({ limit: '50mb' })(req, res, next);
|
||||
} else if (req.path.startsWith('/api')) {
|
||||
|
||||
next();
|
||||
} else {
|
||||
|
||||
express.json()(req, res, next);
|
||||
express.json({ limit: '50mb' })(req, res, next);
|
||||
}
|
||||
});
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
|
||||
|
||||
Reference in New Issue
Block a user