From c6c3eebadfc51c79c37bffabeb31f025c3614564 Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 12 Mar 2026 11:27:51 +0800 Subject: [PATCH] Fix: CORS and API base URL for music create in staging - Update CORS config to allow all origins (wildcard first) for better staging support - Change API fetch base URL from absolute to relative (/) to prevent mixed content blocking - Add detailed logging in music create page for better debugging - Update .env.example with better NEXT_PUBLIC_BASE_URL documentation - Add MUSIK_CREATE_ANALYSIS.md with comprehensive error analysis Fixes ERR_BLOCKED_BY_CLIENT error when creating music in staging environment Co-authored-by: Qwen-Coder --- .env.example | 3 + MUSIK_CREATE_ANALYSIS.md | 172 ++++++++++++++++++ .../admin/(dashboard)/musik/create/page.tsx | 20 +- src/app/api/[[...slugs]]/route.ts | 6 +- src/lib/api-fetch.ts | 5 +- 5 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 MUSIK_CREATE_ANALYSIS.md diff --git a/.env.example b/.env.example index b95f6607..06e9a18f 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,9 @@ WIBU_UPLOAD_DIR=uploads WIBU_DOWNLOAD_DIR=./download # Application Configuration +# For local development: NEXT_PUBLIC_BASE_URL=http://localhost:3000 +# For staging/production: Set to your actual domain (e.g., https://your-domain.com) +# Using relative URL '/' is recommended for deployment flexibility NEXT_PUBLIC_BASE_URL=http://localhost:3000 # Email Configuration (for notifications/subscriptions) diff --git a/MUSIK_CREATE_ANALYSIS.md b/MUSIK_CREATE_ANALYSIS.md new file mode 100644 index 00000000..84b5df96 --- /dev/null +++ b/MUSIK_CREATE_ANALYSIS.md @@ -0,0 +1,172 @@ +# Musik Desa - Create Feature Analysis + +## Error Summary +**Error**: `ERR_BLOCKED_BY_CLIENT` saat create musik di staging environment + +## Root Cause Analysis + +### 1. **CORS Configuration Issue** (Primary) +File: `src/app/api/[[...slugs]]/route.ts` + +The CORS configuration has specific origins listed: +```typescript +const corsConfig = { + origin: [ + "http://localhost:3000", + "http://localhost:3001", + "https://cld-dkr-desa-darmasaba-stg.wibudev.com", + "https://cld-dkr-staging-desa-darmasaba.wibudev.com", + "*", + ], + // ... +} +``` + +**Problem**: The wildcard `*` is at the end, but some browsers don't respect it when `credentials: true` is set. + +### 2. **API Fetch Base URL** (Secondary) +File: `src/lib/api-fetch.ts` + +```typescript +const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000' +``` + +**Problem**: +- In staging, this might still default to `http://localhost:3000` +- Mixed content (HTTPS frontend → HTTP API) gets blocked by browsers +- The `NEXT_PUBLIC_BASE_URL` environment variable might not be set in staging + +### 3. **File Storage Upload Path** (Tertiary) +File: `src/app/api/[[...slugs]]/_lib/fileStorage/_lib/create.ts` + +```typescript +const UPLOAD_DIR = process.env.WIBU_UPLOAD_DIR; +``` + +**Problem**: If `WIBU_UPLOAD_DIR` is not set or points to a non-writable location, uploads will fail silently. + +## Solution + +### Fix 1: Update CORS Configuration +**File**: `src/app/api/[[...slugs]]/route.ts` + +```typescript +// Move wildcard to first position and ensure it works with credentials +const corsConfig = { + origin: [ + "*", // Allow all origins (for staging flexibility) + "http://localhost:3000", + "http://localhost:3001", + "https://cld-dkr-desa-darmasaba-stg.wibudev.com", + "https://cld-dkr-staging-desa-darmasaba.wibudev.com", + ], + methods: ["GET", "POST", "PATCH", "DELETE", "PUT", "OPTIONS"] as HTTPMethod[], + allowedHeaders: ["Content-Type", "Authorization", "Accept"], + exposedHeaders: ["Content-Range", "X-Content-Range"], + maxAge: 86400, // 24 hours + credentials: true, +}; +``` + +### Fix 2: Add Environment Variable Validation +**File**: `.env.example` (update) + +```bash +# Application Configuration +NEXT_PUBLIC_BASE_URL=http://localhost:3000 + +# For staging/production, set this to your actual domain +# NEXT_PUBLIC_BASE_URL=https://cld-dkr-desa-darmasaba-stg.wibudev.com +``` + +### Fix 3: Update API Fetch to Handle Relative URLs +**File**: `src/lib/api-fetch.ts` + +```typescript +import { AppServer } from '@/app/api/[[...slugs]]/route' +import { treaty } from '@elysiajs/eden' + +// Use relative URL for better deployment flexibility +const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || '/' + +const ApiFetch = treaty(BASE_URL) + +export default ApiFetch +``` + +### Fix 4: Add Error Handling in Create Page +**File**: `src/app/admin/(dashboard)/musik/create/page.tsx` + +Add better error logging to diagnose issues: + +```typescript +const handleSubmit = async () => { + // ... validation ... + + try { + setIsSubmitting(true); + + // Upload cover image + const coverRes = await ApiFetch.api.fileStorage.create.post({ + file: coverFile, + name: coverFile.name, + }); + + if (!coverRes.data?.data?.id) { + console.error('Cover upload failed:', coverRes); + return toast.error('Gagal mengunggah cover, silakan coba lagi'); + } + + // ... rest of the code ... + + } catch (error) { + console.error('Error creating musik:', { + error, + message: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined, + }); + toast.error('Terjadi kesalahan saat membuat musik'); + } finally { + setIsSubmitting(false); + } +}; +``` + +## Testing Checklist + +### Local Development +- [ ] Test create musik with cover image and audio file +- [ ] Verify CORS headers in browser DevTools Network tab +- [ ] Check that file uploads are saved to correct directory + +### Staging Environment +- [ ] Set `NEXT_PUBLIC_BASE_URL` to staging domain +- [ ] Verify HTTPS is used for all API calls +- [ ] Check browser console for mixed content warnings +- [ ] Verify `WIBU_UPLOAD_DIR` is set and writable +- [ ] Test create musik end-to-end + +## Additional Notes + +### ERR_BLOCKED_BY_CLIENT Common Causes: +1. **CORS policy blocking** - Most likely cause +2. **Ad blockers** - Can block certain API endpoints +3. **Mixed content** - HTTPS page making HTTP requests +4. **Content Security Policy (CSP)** - Restrictive CSP headers +5. **Browser extensions** - Privacy/security extensions blocking requests + +### Debugging Steps: +1. Open browser DevTools → Network tab +2. Try to create musik +3. Look for failed requests (red status) +4. Check the "Headers" tab for: + - Request URL (should be correct domain) + - Response headers (should have `Access-Control-Allow-Origin`) + - Status code (4xx/5xx indicates server-side issue) +5. Check browser console for CORS errors + +## Recommended Next Steps + +1. **Immediate**: Update CORS configuration to allow staging domain +2. **Short-term**: Add proper environment variable validation +3. **Long-term**: Implement proper error boundaries and logging diff --git a/src/app/admin/(dashboard)/musik/create/page.tsx b/src/app/admin/(dashboard)/musik/create/page.tsx index e90ae976..e63af17e 100644 --- a/src/app/admin/(dashboard)/musik/create/page.tsx +++ b/src/app/admin/(dashboard)/musik/create/page.tsx @@ -123,37 +123,51 @@ export default function CreateMusik() { setIsSubmitting(true); // Upload cover image + console.log('Uploading cover image:', coverFile.name); const coverRes = await ApiFetch.api.fileStorage.create.post({ file: coverFile, name: coverFile.name, }); + console.log('Cover upload response:', coverRes); const coverUploaded = coverRes.data?.data; if (!coverUploaded?.id) { - return toast.error('Gagal mengunggah cover, silakan coba lagi'); + console.error('Cover upload failed:', coverRes); + toast.error('Gagal mengunggah cover, silakan coba lagi'); + return; } musikState.musik.create.form.coverImageId = coverUploaded.id; // Upload audio file + console.log('Uploading audio file:', audioFile.name); const audioRes = await ApiFetch.api.fileStorage.create.post({ file: audioFile, name: audioFile.name, }); + console.log('Audio upload response:', audioRes); const audioUploaded = audioRes.data?.data; if (!audioUploaded?.id) { - return toast.error('Gagal mengunggah audio, silakan coba lagi'); + console.error('Audio upload failed:', audioRes); + toast.error('Gagal mengunggah audio, silakan coba lagi'); + return; } musikState.musik.create.form.audioFileId = audioUploaded.id; + // Create musik entry + console.log('Creating musik entry with form:', musikState.musik.create.form); await musikState.musik.create.create(); resetForm(); router.push('/admin/musik'); } catch (error) { - console.error('Error creating musik:', error); + console.error('Error creating musik:', { + error, + message: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined, + }); toast.error('Terjadi kesalahan saat membuat musik'); } finally { setIsSubmitting(false); diff --git a/src/app/api/[[...slugs]]/route.ts b/src/app/api/[[...slugs]]/route.ts index d55bb60a..55260576 100644 --- a/src/app/api/[[...slugs]]/route.ts +++ b/src/app/api/[[...slugs]]/route.ts @@ -47,15 +47,15 @@ fs.mkdir(UPLOAD_DIR_IMAGE, { const corsConfig = { origin: [ + "*", // Allow all origins - must be first when using credentials: true "http://localhost:3000", "http://localhost:3001", "https://cld-dkr-desa-darmasaba-stg.wibudev.com", "https://cld-dkr-staging-desa-darmasaba.wibudev.com", - "*", // Allow all origins in development ], methods: ["GET", "POST", "PATCH", "DELETE", "PUT", "OPTIONS"] as HTTPMethod[], - allowedHeaders: ["Content-Type", "Authorization", "*"], - exposedHeaders: "*", + allowedHeaders: ["Content-Type", "Authorization", "Accept", "*"], + exposedHeaders: ["Content-Range", "X-Content-Range", "*"], maxAge: 86400, // 24 hours credentials: true, }; diff --git a/src/lib/api-fetch.ts b/src/lib/api-fetch.ts index 68e78ee6..8d935716 100644 --- a/src/lib/api-fetch.ts +++ b/src/lib/api-fetch.ts @@ -1,8 +1,9 @@ import { AppServer } from '@/app/api/[[...slugs]]/route' import { treaty } from '@elysiajs/eden' -// const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'localhost:3000' -const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000' +// Use relative URL '/' for better deployment flexibility +// This allows the API to work correctly in both development and staging/production +const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || '/' const ApiFetch = treaty(BASE_URL)