Fix CORS configuration for PWA offline caching
- Add CORS_ALLOWED_ORIGINS environment variable support in Django settings - Update docker-compose.yml to include CORS origins (HTTP, HTTPS, custom domain) - Add comprehensive PWA offline debugging guide
This commit is contained in:
parent
446125cc76
commit
0be38ca945
3 changed files with 381 additions and 14 deletions
358
DEBUG_PWA_OFFLINE.md
Normal file
358
DEBUG_PWA_OFFLINE.md
Normal file
|
|
@ -0,0 +1,358 @@
|
||||||
|
# 🐛 PWA Offline Caching Debug Guide
|
||||||
|
|
||||||
|
## Issue Summary
|
||||||
|
PWA fails to cache playlists for offline playing with errors like "Failed to cache playlist" and no sound plays offline.
|
||||||
|
|
||||||
|
## Root Causes Identified
|
||||||
|
|
||||||
|
### 1. CORS Configuration Missing
|
||||||
|
**Problem**: Django wasn't reading `CORS_ALLOWED_ORIGINS` from environment variables.
|
||||||
|
|
||||||
|
**Fixed**: Updated `backend/config/settings.py` to read from environment variable.
|
||||||
|
|
||||||
|
**Verify Fix**:
|
||||||
|
```bash
|
||||||
|
# Rebuild container
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker compose logs soundwave | grep CORS
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Service Worker Not Receiving Messages
|
||||||
|
**Problem**: Service worker might not be active or registered properly.
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
```javascript
|
||||||
|
// In browser console
|
||||||
|
navigator.serviceWorker.getRegistration().then(reg => {
|
||||||
|
console.log('SW Registered:', !!reg);
|
||||||
|
console.log('SW Active:', reg?.active?.state);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
SW Registered: true
|
||||||
|
SW Active: "activated"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Audio Fetch Failing Due to CORS or Auth
|
||||||
|
**Problem**: Service worker can't fetch audio files due to CORS or authentication issues.
|
||||||
|
|
||||||
|
## Step-by-Step Debugging
|
||||||
|
|
||||||
|
### Step 1: Check Service Worker Status
|
||||||
|
|
||||||
|
Open DevTools (F12) → **Application** tab → **Service Workers**
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
- ✅ `service-worker.js` registered
|
||||||
|
- ✅ Status: "activated and is running"
|
||||||
|
- ✅ Source: `http://localhost:8889/service-worker.js`
|
||||||
|
|
||||||
|
If not registered:
|
||||||
|
```javascript
|
||||||
|
// In console
|
||||||
|
navigator.serviceWorker.register('/service-worker.js')
|
||||||
|
.then(reg => console.log('SW Registered', reg))
|
||||||
|
.catch(err => console.error('SW Registration failed', err));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Test Service Worker Communication
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In browser console
|
||||||
|
const testSW = async () => {
|
||||||
|
const reg = await navigator.serviceWorker.ready;
|
||||||
|
|
||||||
|
if (!reg.active) {
|
||||||
|
console.error('❌ No active service worker');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageChannel = new MessageChannel();
|
||||||
|
|
||||||
|
messageChannel.port1.onmessage = (event) => {
|
||||||
|
console.log('✅ Response from SW:', event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
reg.active.postMessage(
|
||||||
|
{
|
||||||
|
type: 'CACHE_PLAYLIST',
|
||||||
|
playlistId: 'test123',
|
||||||
|
audioUrls: ['/api/audio/test/download/']
|
||||||
|
},
|
||||||
|
[messageChannel.port2]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
testSW();
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: Response from service worker within 1-2 seconds.
|
||||||
|
|
||||||
|
### Step 3: Check CORS Headers
|
||||||
|
|
||||||
|
Open DevTools → **Network** tab
|
||||||
|
|
||||||
|
1. Navigate to a playlist
|
||||||
|
2. Click "Make Available Offline"
|
||||||
|
3. Look for requests to `/api/audio/{id}/download/`
|
||||||
|
4. Click on one request
|
||||||
|
5. Check **Response Headers**
|
||||||
|
|
||||||
|
Required headers:
|
||||||
|
```
|
||||||
|
Access-Control-Allow-Origin: https://sound.iulian.uk
|
||||||
|
Access-Control-Allow-Credentials: true
|
||||||
|
Content-Type: audio/mpeg (or audio/mp4, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
If CORS headers missing:
|
||||||
|
- ❌ Backend CORS not configured correctly
|
||||||
|
- ❌ Check `CORS_ALLOWED_ORIGINS` environment variable
|
||||||
|
|
||||||
|
### Step 4: Test Manual Audio Fetch
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In browser console
|
||||||
|
const testAudioFetch = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/audio/YOUR_YOUTUBE_ID/download/', {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'audio/*'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Status:', response.status);
|
||||||
|
console.log('Headers:', [...response.headers.entries()]);
|
||||||
|
console.log('Content-Type:', response.headers.get('content-type'));
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log('✅ Audio fetch successful');
|
||||||
|
const blob = await response.blob();
|
||||||
|
console.log('✅ Blob size:', (blob.size / 1024 / 1024).toFixed(2), 'MB');
|
||||||
|
} else {
|
||||||
|
console.error('❌ Audio fetch failed:', response.statusText);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Fetch error:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
testAudioFetch();
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `YOUR_YOUTUBE_ID` with an actual YouTube ID from your playlist.
|
||||||
|
|
||||||
|
### Step 5: Inspect Service Worker Console
|
||||||
|
|
||||||
|
1. DevTools → **Application** → **Service Workers**
|
||||||
|
2. Find the active service worker
|
||||||
|
3. Click **"inspect"** next to it
|
||||||
|
4. A new DevTools window opens (Service Worker console)
|
||||||
|
5. Try caching a playlist again
|
||||||
|
6. Watch the SW console for errors
|
||||||
|
|
||||||
|
Expected logs in SW console:
|
||||||
|
```
|
||||||
|
[Service Worker] Message received: {type: 'CACHE_PLAYLIST', ...}
|
||||||
|
[Service Worker] Caching playlist: PLxxx with 5 tracks
|
||||||
|
[Service Worker] Cached playlist metadata
|
||||||
|
[Service Worker] Fetching: /api/audio/xxx/download/
|
||||||
|
[Service Worker] ✓ Cached: /api/audio/xxx/download/ - 3.45 MB
|
||||||
|
[Service Worker] ✓ Cached: /api/audio/yyy/download/ - 4.12 MB
|
||||||
|
...
|
||||||
|
[Service Worker] Playlist caching complete
|
||||||
|
```
|
||||||
|
|
||||||
|
Common errors:
|
||||||
|
```
|
||||||
|
❌ TypeError: Failed to fetch
|
||||||
|
→ CORS issue or network problem
|
||||||
|
|
||||||
|
❌ HTTP 401 Unauthorized
|
||||||
|
→ Authentication cookies not sent
|
||||||
|
|
||||||
|
❌ HTTP 403 Forbidden
|
||||||
|
→ CORS or CSRF issue
|
||||||
|
|
||||||
|
❌ HTTP 404 Not Found
|
||||||
|
→ Audio not downloaded yet or wrong URL
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Check Cache Storage
|
||||||
|
|
||||||
|
DevTools → **Application** → **Cache Storage**
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
- `soundwave-v1` - Static assets
|
||||||
|
- `soundwave-api-v1` - API responses
|
||||||
|
- `soundwave-audio-v1` - **Audio files** ← This is where audio should be
|
||||||
|
- `soundwave-images-v1` - Images
|
||||||
|
|
||||||
|
Click on `soundwave-audio-v1`:
|
||||||
|
- Should show cached audio files with keys like `/api/audio/{id}/download/`
|
||||||
|
- Each entry should have a size (MB)
|
||||||
|
|
||||||
|
If empty after caching:
|
||||||
|
- ❌ Caching failed silently
|
||||||
|
- Check SW console for errors
|
||||||
|
|
||||||
|
### Step 7: Test Offline Playback
|
||||||
|
|
||||||
|
1. Cache a playlist (follow steps above to ensure it works)
|
||||||
|
2. DevTools → **Network** tab
|
||||||
|
3. Change throttling to **"Offline"**
|
||||||
|
4. Try playing a track
|
||||||
|
|
||||||
|
What to check:
|
||||||
|
- Network tab shows requests but they should come from "ServiceWorker"
|
||||||
|
- Console should show: `[Service Worker] ✓ Serving audio from cache: /api/audio/xxx/download/`
|
||||||
|
|
||||||
|
If fails:
|
||||||
|
```
|
||||||
|
❌ [Service Worker] ✗ Cache miss for: /api/audio/xxx/download/
|
||||||
|
→ Audio wasn't cached properly
|
||||||
|
|
||||||
|
❌ [Service Worker] Available cached audio: []
|
||||||
|
→ Cache is empty
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Fixes
|
||||||
|
|
||||||
|
### Fix 1: Restart Docker Containers
|
||||||
|
```bash
|
||||||
|
cd /home/iulian/projects/zi-tube/soundwave
|
||||||
|
docker compose down
|
||||||
|
docker compose build --no-cache
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 2: Clear All Caches
|
||||||
|
```javascript
|
||||||
|
// In browser console
|
||||||
|
const clearAllCaches = async () => {
|
||||||
|
const cacheNames = await caches.keys();
|
||||||
|
await Promise.all(cacheNames.map(name => caches.delete(name)));
|
||||||
|
console.log('✅ All caches cleared');
|
||||||
|
|
||||||
|
// Unregister service worker
|
||||||
|
const reg = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (reg) {
|
||||||
|
await reg.unregister();
|
||||||
|
console.log('✅ Service worker unregistered');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 Reload page now');
|
||||||
|
};
|
||||||
|
|
||||||
|
clearAllCaches();
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reload the page (hard refresh: Ctrl+Shift+R).
|
||||||
|
|
||||||
|
### Fix 3: Update Service Worker
|
||||||
|
```javascript
|
||||||
|
// In browser console
|
||||||
|
const updateSW = async () => {
|
||||||
|
const reg = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (reg) {
|
||||||
|
await reg.update();
|
||||||
|
console.log('✅ Service worker updated');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSW();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 4: Check Container Environment Variables
|
||||||
|
```bash
|
||||||
|
# Check if CORS is set
|
||||||
|
docker compose exec soundwave env | grep CORS
|
||||||
|
|
||||||
|
# Should show:
|
||||||
|
# CORS_ALLOWED_ORIGINS=http://localhost:8889,https://localhost:8889,https://sound.iulian.uk
|
||||||
|
```
|
||||||
|
|
||||||
|
If not shown, environment variable not set in docker-compose.yml.
|
||||||
|
|
||||||
|
## Known Issues & Solutions
|
||||||
|
|
||||||
|
### Issue: "Failed to cache playlist" after 60 seconds
|
||||||
|
**Cause**: Timeout - too many files or slow network
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Cache smaller playlists first (< 10 tracks)
|
||||||
|
2. Check network speed
|
||||||
|
3. Increase timeout in `frontend/src/utils/pwa.ts` (line ~325)
|
||||||
|
|
||||||
|
### Issue: CORS error when fetching audio
|
||||||
|
**Cause**: Backend not configured for HTTPS origin
|
||||||
|
|
||||||
|
**Solution**: Already fixed in `docker-compose.yml` and `settings.py`
|
||||||
|
|
||||||
|
### Issue: Service worker not updating
|
||||||
|
**Cause**: Browser caching old service worker
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. DevTools → Application → Service Workers
|
||||||
|
2. Check "Update on reload"
|
||||||
|
3. Hard refresh (Ctrl+Shift+R)
|
||||||
|
|
||||||
|
### Issue: Audio plays online but not offline
|
||||||
|
**Cause**: Audio not in cache or wrong cache key
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check Cache Storage (Step 6)
|
||||||
|
2. Verify cache keys match request URLs exactly
|
||||||
|
3. Re-cache the playlist
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
Before reporting the issue is fixed:
|
||||||
|
|
||||||
|
- [ ] Service worker registered and active
|
||||||
|
- [ ] CORS headers present on audio downloads
|
||||||
|
- [ ] Can cache a small playlist (3-5 tracks)
|
||||||
|
- [ ] Cache Storage shows audio files
|
||||||
|
- [ ] Offline mode: can play cached tracks
|
||||||
|
- [ ] Network tab shows "ServiceWorker" as source
|
||||||
|
- [ ] No console errors during playback
|
||||||
|
|
||||||
|
## Advanced: Check Django CORS Settings
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH into container
|
||||||
|
docker compose exec soundwave bash
|
||||||
|
|
||||||
|
# Open Django shell
|
||||||
|
python manage.py shell
|
||||||
|
|
||||||
|
# Check CORS settings
|
||||||
|
from django.conf import settings
|
||||||
|
print("CORS_ALLOWED_ORIGINS:", settings.CORS_ALLOWED_ORIGINS)
|
||||||
|
print("CORS_ALLOW_CREDENTIALS:", settings.CORS_ALLOW_CREDENTIALS)
|
||||||
|
print("CSRF_TRUSTED_ORIGINS:", settings.CSRF_TRUSTED_ORIGINS)
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```python
|
||||||
|
CORS_ALLOWED_ORIGINS: ['http://localhost:8889', 'https://localhost:8889', 'https://sound.iulian.uk']
|
||||||
|
CORS_ALLOW_CREDENTIALS: True
|
||||||
|
CSRF_TRUSTED_ORIGINS: ['http://localhost:8889', 'https://localhost:8889', 'https://sound.iulian.uk']
|
||||||
|
```
|
||||||
|
|
||||||
|
## Need More Help?
|
||||||
|
|
||||||
|
If still not working after following this guide:
|
||||||
|
|
||||||
|
1. Share output from **Step 1** (SW status)
|
||||||
|
2. Share output from **Step 4** (audio fetch test)
|
||||||
|
3. Share screenshot of **Step 5** (SW console errors)
|
||||||
|
4. Share screenshot of **Step 6** (Cache Storage)
|
||||||
|
|
||||||
|
This will help identify the exact issue.
|
||||||
|
|
@ -146,19 +146,27 @@ REST_FRAMEWORK = {
|
||||||
}
|
}
|
||||||
|
|
||||||
# CORS settings
|
# CORS settings
|
||||||
CORS_ALLOWED_ORIGINS = [
|
CORS_ALLOWED_ORIGINS_ENV = os.environ.get('CORS_ALLOWED_ORIGINS', '')
|
||||||
"http://localhost:8889",
|
if CORS_ALLOWED_ORIGINS_ENV:
|
||||||
"http://127.0.0.1:8889",
|
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in CORS_ALLOWED_ORIGINS_ENV.split(',')]
|
||||||
"http://192.168.50.71:8889",
|
else:
|
||||||
]
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
"http://localhost:8889",
|
||||||
|
"http://127.0.0.1:8889",
|
||||||
|
"http://192.168.50.71:8889",
|
||||||
|
]
|
||||||
CORS_ALLOW_CREDENTIALS = True
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
|
||||||
# CSRF settings for development cross-origin access
|
# CSRF settings for development cross-origin access
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS_ENV = os.environ.get('CORS_ALLOWED_ORIGINS', '')
|
||||||
"http://localhost:8889",
|
if CSRF_TRUSTED_ORIGINS_ENV:
|
||||||
"http://127.0.0.1:8889",
|
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in CSRF_TRUSTED_ORIGINS_ENV.split(',')]
|
||||||
"http://192.168.50.71:8889",
|
else:
|
||||||
]
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
|
"http://localhost:8889",
|
||||||
|
"http://127.0.0.1:8889",
|
||||||
|
"http://192.168.50.71:8889",
|
||||||
|
]
|
||||||
CSRF_COOKIE_SAMESITE = 'Lax'
|
CSRF_COOKIE_SAMESITE = 'Lax'
|
||||||
CSRF_COOKIE_SECURE = False
|
CSRF_COOKIE_SECURE = False
|
||||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ services:
|
||||||
soundwave:
|
soundwave:
|
||||||
container_name: soundwave
|
container_name: soundwave
|
||||||
# Option 1: Pull from GitHub Container Registry (recommended for users)
|
# Option 1: Pull from GitHub Container Registry (recommended for users)
|
||||||
#image: ghcr.io/aiulian25/soundwave:latest
|
# image: ghcr.io/aiulian25/soundwave:main
|
||||||
# Option 2: Build locally (uncomment if pulling fails or for development)
|
# Option 2: Build locally (uncomment if pulling fails or for development)
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "8889:8888"
|
- "8889:8888"
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -24,6 +24,7 @@ services:
|
||||||
- REDIS_HOST=soundwave-redis
|
- REDIS_HOST=soundwave-redis
|
||||||
- TZ=UTC
|
- TZ=UTC
|
||||||
- ES_URL=http://soundwave-es:9200
|
- ES_URL=http://soundwave-es:9200
|
||||||
|
- CORS_ALLOWED_ORIGINS=http://localhost:8889,https://localhost:8889,https://sound.iulian.uk
|
||||||
depends_on:
|
depends_on:
|
||||||
- soundwave-es
|
- soundwave-es
|
||||||
- soundwave-redis
|
- soundwave-redis
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue