From 1014475c0fa138b6da748d5c50a22c9e3d4f36cb Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Wed, 14 Jan 2026 20:16:17 -0600 Subject: [PATCH 1/2] fix: add dynamic timeout for document uploads (refs #33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document uploads were failing with "timeout of 10000ms exceeded" error because the global axios client timeout (10s) was too short for medium-sized files (1-5MB). Added calculateUploadTimeout() function that calculates timeout based on file size: 30s base + 10s per MB. This allows uploads to complete on slower connections while still having reasonable timeout limits. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/features/documents/api/documents.api.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frontend/src/features/documents/api/documents.api.ts b/frontend/src/features/documents/api/documents.api.ts index a6d7ef8..c1a0533 100644 --- a/frontend/src/features/documents/api/documents.api.ts +++ b/frontend/src/features/documents/api/documents.api.ts @@ -1,6 +1,17 @@ import { apiClient } from '../../../core/api/client'; import type { CreateDocumentRequest, DocumentRecord, UpdateDocumentRequest } from '../types/documents.types'; +/** + * Calculate upload timeout based on file size. + * Base: 30 seconds + 10 seconds per MB to accommodate slow connections. + */ +function calculateUploadTimeout(file: File): number { + const fileSizeMB = file.size / (1024 * 1024); + const baseTimeout = 30000; // 30 seconds minimum + const perMBTimeout = 10000; // 10 seconds per MB + return Math.round(baseTimeout + fileSizeMB * perMBTimeout); +} + export const documentsApi = { async list(params?: { vehicleId?: string; type?: string; expiresBefore?: string }) { const res = await apiClient.get('/documents', { params }); @@ -26,6 +37,7 @@ export const documentsApi = { form.append('file', file); const res = await apiClient.post(`/documents/${id}/upload`, form, { headers: { 'Content-Type': 'multipart/form-data' }, + timeout: calculateUploadTimeout(file), }); return res.data; }, @@ -34,6 +46,7 @@ export const documentsApi = { form.append('file', file); const res = await apiClient.post(`/documents/${id}/upload`, form, { headers: { 'Content-Type': 'multipart/form-data' }, + timeout: calculateUploadTimeout(file), onUploadProgress: (evt) => { if (evt.total) { const pct = Math.round((evt.loaded / evt.total) * 100); -- 2.49.1 From a3b119a95354f5a498d8c39567e74d4e5b1629cc Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Wed, 14 Jan 2026 20:28:19 -0600 Subject: [PATCH 2/2] fix: resolve document upload hang by fixing stream pipeline (refs #33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upload was hanging silently because breaking early from a `for await` loop on a Node.js stream corrupts the stream's internal state. The remaining stream could not be used afterward. Changes: - Collect ALL chunks from the file stream before processing - Use subarray() for file type detection header (first 4100 bytes) - Create single readable stream from complete buffer for storage - Remove broken headerStream + remainingStream piping logic This fixes the root cause where uploads would hang after logging "Document upload requested" without ever completing or erroring. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../documents/api/documents.controller.ts | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/backend/src/features/documents/api/documents.controller.ts b/backend/src/features/documents/api/documents.controller.ts index 00caa67..df017a8 100644 --- a/backend/src/features/documents/api/documents.controller.ts +++ b/backend/src/features/documents/api/documents.controller.ts @@ -272,20 +272,15 @@ export class DocumentsController { }); } - // Read first 4100 bytes to detect file type via magic bytes + // Collect ALL file chunks first (breaking early from async iterator corrupts stream state) const chunks: Buffer[] = []; - let totalBytes = 0; - const targetBytes = 4100; - for await (const chunk of mp.file) { chunks.push(chunk); - totalBytes += chunk.length; - if (totalBytes >= targetBytes) { - break; - } } + const fullBuffer = Buffer.concat(chunks); - const headerBuffer = Buffer.concat(chunks); + // Use first 4100 bytes for file type detection via magic bytes + const headerBuffer = fullBuffer.subarray(0, Math.min(4100, fullBuffer.length)); // Validate actual file content using magic bytes const detectedType = await FileType.fromBuffer(headerBuffer); @@ -341,15 +336,9 @@ export class DocumentsController { const counter = new CountingStream(); - // Create a new readable stream from the header buffer + remaining file chunks - const headerStream = Readable.from([headerBuffer]); - const remainingStream = mp.file; - - // Pipe header first, then remaining content through counter - headerStream.pipe(counter, { end: false }); - headerStream.on('end', () => { - remainingStream.pipe(counter); - }); + // Create readable stream from the complete buffer and pipe through counter + const fileStream = Readable.from([fullBuffer]); + fileStream.pipe(counter); const storage = getStorageService(); const bucket = 'documents'; -- 2.49.1