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
This commit is contained in:
1 parent
0abcead359
commit
0fdb4e0bc3
3 files changed
+102
-6
No files matched your search
@@ -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
|
||||||
@@ -33,7 +33,14 @@ export const GET = withAdmin(
|
|||||||
|
|
||||||
// GET ?source=<id>&action=clonable → all not-yet-present classnames (for "clone all")
|
// GET ?source=<id>&action=clonable → all not-yet-present classnames (for "clone all")
|
||||||
if (sp.get("action") === "clonable") {
|
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") || "";
|
const search = sp.get("search") || "";
|
||||||
@@ -48,12 +55,19 @@ export const GET = withAdmin(
|
|||||||
? filterParam
|
? filterParam
|
||||||
: "all";
|
: "all";
|
||||||
|
|
||||||
const [listResult, stats] = await Promise.all([
|
try {
|
||||||
getCloneList({ source, search, page, perPage, filter }),
|
const [listResult, stats] = await Promise.all([
|
||||||
getCloneStats(source),
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,11 @@ export async function fetchSourceFurnidata(
|
|||||||
} catch {
|
} catch {
|
||||||
body = await fetchWithFlareSolver(url);
|
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[];
|
let list: SourceFurni[];
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(body);
|
const json = JSON.parse(body);
|
||||||
|
|||||||
Reference in new issue
Block a user