From 0fdb4e0bc3e4b1d0031fd503a4afccd802adb63b Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 4 Aug 2026 19:14:44 +0200 Subject: [PATCH] fix: improve error handling for Cloudflare-protected clone sources - Detect HTML responses from FlareSolverr and throw descriptive error instead of letting JSON.parse fail with cryptic 'Unexpected token' error - Add try/catch to clone/route.ts GET handler to return 502 with clear message instead of Internal Server Error 500 - Add FLARESOLVERR_URL to .env --- .env | 77 +++++++++++++++++++++++++ src/app/api/admin/import/clone/route.ts | 26 +++++++-- src/lib/services/clone-import.ts | 5 ++ 3 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 00000000..a03dea4c --- /dev/null +++ b/.env @@ -0,0 +1,77 @@ +# ============================================================================== +# Epicnextcms — Ultimate Speed & Low-Latency Production Configuration +# ============================================================================== + +# --- DATABASE (High Performance Pooling & Strict Timeouts) --- +DATABASE_URL="mysql://cms:%4019Poede96@localhost:3306/habbo?charset=utf8mb4&connection_limit=150&connect_timeout=5" +DATABASE_POOL_SIZE=150 +DATABASE_IDLE_TIMEOUT_MS=60000 +DATABASE_CONNECT_TIMEOUT_MS=5000 + +# --- REDIS (Lightning Fast Caching & Sessions) --- +REDIS_URL=redis://127.0.0.1:6379?connect_timeout=2 +REDIS_CACHE_TTL_DEFAULT=7200 + +# --- CORE RUNTIME & PERFORMANCE FLAGS --- +NODE_ENV=production +PORT=3002 +NEXT_TELEMETRY_DISABLED=1 +UV_THREADPOOL_SIZE=16 + +# --- HOTEL & URLS --- +HOTEL_NAME=Epicnabbo +APP_URL=https://epicnabbo.nl +NEXT_PUBLIC_APP_URL=https://epicnabbo.nl +AUTH_URL=https://epicnabbo.nl + +# --- IMAGER --- +IMAGING_UPSTREAM_URL=http://127.0.0.1:3030/imaging +NEXT_PUBLIC_IMAGER_URL=https://epicnabbo.nl/imaging + +# --- SECURITY & HASHING (High Performance) --- +AUTH_SECRET="sBG+BJ7dGKN3oRW328lbtdIRcDaGEWMFMdDXR7xG8vc=" +APP_KEY="base64:oShuiGJ5UAgqmJ5AppLX8PBJs6LEtEOdoQILwcYcwao=" +CONVERT_PASSWORDS=true +# CONVERT_PASSWORDS: enables legacy md5/argon2id -> bcrypt upgrade on login. +BCRYPT_COST=12 + +# --- PATHS --- +BADGE_UPLOAD_DIR=./public/assets/images/badges +EMULATOR_JAR_PATH=./emulator/Arcturus.jar +EMULATOR_BACKUP_DIR=./backups/emulator +EMULATOR_BACKUP_KEEP=7 +# Optional mysqldump (jobs-worker daily 03:30). Requires mysqldump on PATH. +DB_BACKUP_DIR= +DB_BACKUP_KEEP=7 +# Minutes between repeat health-fail alerts from jobs-worker (default 15). +HEALTH_ALERT_COOLDOWN_MIN=15 + +# --- RCON (Low Latency Loop) --- +RCON_HOST=127.0.0.1 +RCON_PORT=3003 +RCON_TIMEOUT_MS=2000 + +# --- EMAIL & NOTIFICATIONS --- +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASSWORD= +SMTP_FROM=no-reply@epicnabbo.nl + +# --- ALERTING & MONITORING --- +DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/placeholder" +ALERT_EMAIL= + +# --- MODERATION & PAYMENTS --- +OPENAI_API_KEY= +PAYPAL_CLIENT_ID= +PAYPAL_SECRET= +PAYPAL_API=https://api-m.sandbox.paypal.com + +# --- LOGGING & SENTRY (Zod-Proof Dummy URLs) --- +LOG_LEVEL=error +SENTRY_DSN= +NEXT_PUBLIC_SENTRY_DSN= + +# --- FLARESOLVERR (Cloudflare bypass for clone sources) --- +FLARESOLVERR_URL=http://localhost:8191 \ No newline at end of file diff --git a/src/app/api/admin/import/clone/route.ts b/src/app/api/admin/import/clone/route.ts index e2a5a69e..8b3fcca3 100644 --- a/src/app/api/admin/import/clone/route.ts +++ b/src/app/api/admin/import/clone/route.ts @@ -33,7 +33,14 @@ export const GET = withAdmin( // GET ?source=&action=clonable → all not-yet-present classnames (for "clone all") if (sp.get("action") === "clonable") { - return apiOk({ classnames: await getClonableClassnames(source) }); + try { + return apiOk({ classnames: await getClonableClassnames(source) }); + } catch (err) { + return apiError( + `Failed to fetch source data: ${(err as Error).message}`, + 502, + ); + } } const search = sp.get("search") || ""; @@ -48,12 +55,19 @@ export const GET = withAdmin( ? filterParam : "all"; - const [listResult, stats] = await Promise.all([ - getCloneList({ source, search, page, perPage, filter }), - getCloneStats(source), - ]); + try { + const [listResult, stats] = await Promise.all([ + getCloneList({ source, search, page, perPage, filter }), + getCloneStats(source), + ]); - return apiOk({ items: listResult.items, meta: listResult.meta, stats }); + return apiOk({ items: listResult.items, meta: listResult.meta, stats }); + } catch (err) { + return apiError( + `Failed to fetch source data: ${(err as Error).message}`, + 502, + ); + } }, ); diff --git a/src/lib/services/clone-import.ts b/src/lib/services/clone-import.ts index bbc8021e..8c4891ef 100644 --- a/src/lib/services/clone-import.ts +++ b/src/lib/services/clone-import.ts @@ -106,6 +106,11 @@ export async function fetchSourceFurnidata( } catch { body = await fetchWithFlareSolver(url); } + if (body.trimStart().startsWith("<")) { + throw new Error( + `Source ${url} is behind a Cloudflare challenge that could not be bypassed. Start FlareSolverr and check its logs.`, + ); + } let list: SourceFurni[]; try { const json = JSON.parse(body);