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")
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user