fix: improve error handling for Cloudflare-protected clone sources
CI / check (push) Successful in 27s
CI / release (push) Skipped
CI / deploy (push) Successful in 48s

- 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
This commit is contained in:
openhands committed 2026-08-04 19:14:44 +02:00
1 parent 0abcead359
commit 0fdb4e0bc3
3 files changed
+102 -6

No files matched your search

+77
View File
@@ -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=[email protected]
# --- 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
+20 -6
View File
@@ -33,7 +33,14 @@ export const GET = withAdmin(
// GET ?source=<id>&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,
);
}
},
);
+5
View File
@@ -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);