🚨 PLAYBOOK: UPLOAD FAIL (CARGA INFINITA)
🩺 SÍNTOMA
El usuario intenta subir una imagen, documento o archivo y la UI se queda en estado de "Loading" o "Subiendo..." indefinidamente. No hay mensaje de error, no pasa nada.
⏱️ DIAGNÓSTICO EN 30 SEGUNDOS
- Frontend: Abre la pestaña Network (DevTools). Busca el request POST/PUT. ¿Sigue en
pending? ¿TiróCORS error? ¿Dio HTTP 413 (Payload Too Large)? - Backend: Abre el dashboard de Cloudflare Workers. Busca excepciones recientes o Memory Limit Exceeded.
- Storage: Revisa R2. ¿El archivo llegó pero el Worker no respondió?
🔍 CAUSAS Y SOLUCIONES DE HIERRO
Causa 1: Timeout en el Frontend (Request Colgado)
Si la red es inestable, el request se cuelga eternamente. Solución: ES OBLIGATORIO usar un AbortController con timeout. PROHIBIDO hacer fetch ciego.
Causa 2: Worker Sin Memoria (128MB Default)
Si el archivo se procesa en el Worker (request.arrayBuffer()) y pesa más de 50MB, el Worker colapsa por RAM. Solución: NUNCA procesar archivos grandes en RAM. Usar Streams directo a R2, o bien aumentar la memoria si es estrictamente necesario (no recomendado).
# wrangler.toml (Si necesitas más de 128MB, usar Unbound)
[limits]
memory = 256 # MáximoCausa 3: Archivo Excede el Límite de Cloudflare (100MB)
Cloudflare free/pro bloquea payloads > 100MB antes de tocar tu Worker. Solución: ES OBLIGATORIO validar el tamaño del archivo en el cliente ANTES de enviarlo.
Causa 4: Error de CORS
Si la subida va directa a un bucket R2 presigned, el CORS del bucket bloquea el PUT. Solución: Configurar las reglas de CORS en el bucket R2.
[
{
"AllowedOrigins": ["https://tu-app.com"],
"AllowedMethods": ["PUT", "POST", "GET"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}
]Causa 5: R2 Bucket No Existe o Permisos Rotos
El binding no está configurado en producción. Solución: Validar el wrangler.toml y asegurar que [[r2_buckets]] apunte al bucket correcto en producción.
💻 CÓDIGO REAL: HOOK DE UPLOAD BLINDADO
ES OBLIGATORIO usar este hook (o su equivalente) para subir archivos. NUNCA crear implementaciones caseras sin timeout ni validación.
import { useState, useRef } from 'react'
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
const TIMEOUT_MS = 15000 // 15 segundos máximo
export function useFileUpload() {
const [isUploading, setIsUploading] = useState(false)
const [progress, setProgress] = useState(0)
const [error, setError] = useState<string | null>(null)
const abortControllerRef = useRef<AbortController | null>(null)
const uploadFile = async (file: File, uploadUrl: string) => {
// 1. VALIDACIONES INQUEBRANTABLES
if (file.size > MAX_FILE_SIZE) {
setError('El archivo excede el límite de 5MB.')
return false
}
if (!ALLOWED_TYPES.includes(file.type)) {
setError('Formato no permitido. Solo JPG, PNG o PDF.')
return false
}
setIsUploading(true)
setError(null)
setProgress(0)
// 2. TIMEOUT CON ABORT CONTROLLER
abortControllerRef.current = new AbortController()
const timeoutId = setTimeout(() => {
abortControllerRef.current?.abort()
}, TIMEOUT_MS)
try {
// Simular progreso en frontend (XMLHttpRequest es necesario para progreso real,
// pero fetch no lo soporta nativo para upload, así que usamos un timer falso o delegamos al SDK)
const progressInterval = setInterval(() => setProgress(p => Math.min(p + 10, 90)), 500)
const response = await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
signal: abortControllerRef.current.signal
})
clearInterval(progressInterval)
clearTimeout(timeoutId)
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`)
}
setProgress(100)
return true
} catch (err: unknown) {
clearTimeout(timeoutId)
if (err instanceof Error && err.name === 'AbortError') {
setError('Tiempo de espera agotado. Revisa tu conexión.')
} else {
setError('No pudimos subir el archivo. Inténtalo de nuevo.')
}
return false
} finally {
setIsUploading(false)
abortControllerRef.current = null
}
}
const cancelUpload = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
setError('Subida cancelada por el usuario.')
}
}
return { uploadFile, cancelUpload, isUploading, progress, error }
}