Fix upload stuck pending: validate blob readability before upload

iOS Safari can evict blob data from IndexedDB while keeping the Blob
reference intact (instanceof/size checks pass but actual data read
fails). This caused upload POST to throw 'Load failed' client-side,
never reaching the server, burning retries endlessly.

Changes:
- Add validateBlobReadable() that reads first byte to verify data access
- If blob is unreadable, mark as 'failed' immediately with clear message
- Re-read photo from IndexedDB before upload for fresh blob reference
- Capture detailed error name/type in catch for better diagnostics
- Bump SW cache versions to v14/v10 to force new code delivery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 15:56:06 -06:00
parent bc748d05ac
commit 1da3eea7b8
2 changed files with 52 additions and 13 deletions

View File

@@ -129,15 +129,39 @@ const Sync = {
const retryCount = photo.retryCount || 0;
try {
// Re-read photo from IndexedDB to get a fresh blob reference
// (iOS Safari can invalidate blob references between reads)
const freshPhoto = await Storage.db.photos.get(photo.id);
if (!freshPhoto || (freshPhoto.status !== 'pending' && freshPhoto.status !== 'uploading')) {
console.log('[SYNC] Photo no longer pending, skipping:', photo.id);
return true;
}
const blob = freshPhoto.blob;
console.log('[SYNC] Uploading:', photo.filename,
'(' + (photo.blob ? (photo.blob.size / 1024 / 1024).toFixed(1) + 'MB' : 'no blob') + ')',
'(' + (blob ? (blob.size / 1024 / 1024).toFixed(1) + 'MB' : 'no blob') + ')',
'attempt', retryCount + 1, 'of', this.MAX_RETRIES);
// Validate blob before attempting upload
if (!photo.blob || !(photo.blob instanceof Blob) || photo.blob.size === 0) {
// Validate blob reference exists
if (!blob || !(blob instanceof Blob) || blob.size === 0) {
throw new Error('Photo data is missing or corrupted - please delete and re-capture');
}
// Actually try to read the blob to verify data is accessible
// (iOS Safari can evict blob data from IndexedDB while keeping the reference)
const readable = await this.validateBlobReadable(blob);
if (!readable) {
// Blob data is gone — no amount of retrying will fix this
console.error('[SYNC] Blob data is no longer readable:', photo.filename);
await Storage.updatePhoto(photo.id, {
status: 'failed',
lastError: 'Photo data was lost by the browser - please re-capture',
error: 'Photo data was lost by the browser - please re-capture'
});
return false;
}
await Storage.updatePhoto(photo.id, { status: 'uploading' });
// Prevent page navigation during upload
@@ -162,10 +186,8 @@ const Sync = {
}
}
const uploadBlob = photo.blob;
const formData = new FormData();
formData.append('file', uploadBlob, photo.filename);
formData.append('file', blob, photo.filename);
const uploadUrl = '/api/files/upload?path=' + encodeURIComponent(photo.targetPath);
@@ -185,7 +207,10 @@ const Sync = {
if (fetchErr.name === 'AbortError') {
throw new Error('Upload timed out (' + (this.UPLOAD_TIMEOUT_MS / 1000) + 's)');
}
throw fetchErr;
// Capture detailed error info for debugging
const errDetail = fetchErr.name + ': ' + fetchErr.message;
console.error('[SYNC] Fetch threw:', errDetail);
throw new Error('Network error: ' + errDetail);
}
clearTimeout(timeoutId);
@@ -241,12 +266,13 @@ const Sync = {
} catch (error) {
this._setUploading(false);
console.error('[SYNC] Upload failed:', error.message, '(attempt', retryCount + 1 + ')');
const errMsg = (error.name || 'Error') + ': ' + error.message;
console.error('[SYNC] Upload failed:', errMsg, '(attempt', retryCount + 1 + ')');
await Storage.updatePhoto(photo.id, {
status: 'pending',
retryCount: retryCount + 1,
lastError: error.message,
error: error.message
lastError: errMsg,
error: errMsg
});
return false;
}
@@ -268,6 +294,19 @@ const Sync = {
}
},
async validateBlobReadable(blob) {
try {
// Try to read the first byte — if iOS has evicted the data,
// this will throw even though the Blob reference looks valid
const slice = blob.slice(0, 1);
const buf = await slice.arrayBuffer();
return buf.byteLength > 0;
} catch (e) {
console.error('[SYNC] Blob readability check failed:', e.message);
return false;
}
},
async checkFileExists(path) {
try {
const verifyUrl = '/api/files/verify?path=' + encodeURIComponent(path);