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:
@@ -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);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// NextSnap Service Worker
|
||||
// Provides offline-first caching for the app shell
|
||||
|
||||
const CACHE_VERSION = 'nextsnap-v13';
|
||||
const APP_SHELL_CACHE = 'nextsnap-shell-v9';
|
||||
const RUNTIME_CACHE = 'nextsnap-runtime-v9';
|
||||
const CACHE_VERSION = 'nextsnap-v14';
|
||||
const APP_SHELL_CACHE = 'nextsnap-shell-v10';
|
||||
const RUNTIME_CACHE = 'nextsnap-runtime-v10';
|
||||
|
||||
// Assets to cache on install
|
||||
const APP_SHELL_ASSETS = [
|
||||
|
||||
Reference in New Issue
Block a user