fix: show a proper error message when MakerWorld cookie fails due flareSolverr

This commit is contained in:
Tautvydas Derzinskas committed 2026-09-21 20:27:14 +02:00
1 parent 46b8687d1d
commit a81765e1ca
2 files changed
+40 -13

No files matched your search

+22 -6
View File
@@ -28,7 +28,7 @@ import {
validateStorageTemplate,
} from "../services/printService";
import { getUserMakerworldCookie, setUserMakerworldCookie } from "../services/makerworldCookieService";
import { verifyMakerworldCookie } from "../services/makerworldCloudApi";
import { type MakerworldCookieCheck, verifyMakerworldCookie } from "../services/makerworldCloudApi";
import { verifyThingiverseAccessToken } from "../services/thingiverseApi";
import { SLICER_IDS, getUserSlicer, setUserSlicer } from "../services/slicerPreferenceService";
import { THEME_SELECTIONS, getUserTheme, setUserTheme } from "../services/themePreferenceService";
@@ -282,6 +282,17 @@ router.get(
// tab) and deliberately swallows failures, so adding a live network test there would only add
// latency for no benefit. Only ProfilePage's manual Save (settingsApi.updateMakerworld) sends
// `verify: true`.
const MAKERWORLD_UNVERIFIABLE_MESSAGES: Record<
Extract<MakerworldCookieCheck, { result: "unverifiable" }>["reason"],
string
> = {
cloudflare_no_flaresolverr:
"Couldn't check this cookie: MakerWorld's Cloudflare protection blocked the request and FlareSolverr isn't configured. Set FLARESOLVERR_URL and try again.",
flaresolverr_failed:
"Couldn't check this cookie: MakerWorld's Cloudflare protection blocked the request and FlareSolverr didn't get past it. Make sure FlareSolverr is running and reachable at FLARESOLVERR_URL, then try again.",
network: "Couldn't check this cookie: MakerWorld didn't respond. Check this server's internet connection and try again.",
};
const makerworldSettingsSchema = z.object({ cookie: z.string().nullable(), verify: z.boolean().optional() });
router.patch(
"/settings/makerworld",
@@ -290,11 +301,16 @@ router.patch(
const trimmed = (body.cookie ?? "").trim();
// Only a new, non-empty cookie needs testing -- clearing it (trimmed === "") always
// succeeds, since there's nothing to verify a logged-out state against.
if (body.verify && trimmed && !(await verifyMakerworldCookie(trimmed))) {
throw new HttpError(
422,
"Couldn't verify this MakerWorld cookie -- it may be invalid or expired. Copy a fresh Cookie header from a logged-in makerworld.com tab and try again.",
);
if (body.verify && trimmed) {
const check = await verifyMakerworldCookie(trimmed);
if (check.result === "invalid") {
throw new HttpError(
422,
"MakerWorld rejected this cookie -- it may be invalid or expired. Copy a fresh Cookie header from a logged-in makerworld.com tab and try again.",
);
}
// 503, not 422: nothing is known to be wrong with the cookie, the check itself couldn't run.
if (check.result === "unverifiable") throw new HttpError(503, MAKERWORLD_UNVERIFIABLE_MESSAGES[check.reason]);
}
const configured = await setUserMakerworldCookie(req.userId!, body.cookie);
res.json({ configured });
+18 -7
View File
@@ -101,15 +101,23 @@ export function extractMakerworldBearerToken(rawCookieOrToken: string | null | u
return null;
}
/** Outcome of verifyMakerworldCookie. "unverifiable" means the check never got an answer about
* the cookie itself -- Cloudflare challenged the request and FlareSolverr is unset, unreachable,
* or couldn't solve it, or MakerWorld didn't respond -- so the cookie may be perfectly fine. */
export type MakerworldCookieCheck =
| { result: "valid" }
| { result: "invalid" }
| { result: "unverifiable"; reason: "cloudflare_no_flaresolverr" | "flaresolverr_failed" | "network" };
/** Called before storing a cookie pasted into Profile > MakerWorld (see routes/settings.ts'
* PATCH /settings/makerworld), so a stale/expired/mistyped paste is rejected up front instead of
* only surfacing as a failed import later. A malformed paste (extractMakerworldBearerToken
* finding no usable token at all) is rejected without a network call; otherwise this is the
* same self-profile request api.bambulab.com/MakerWorld clients use right after login, so a 200
* here means the account is genuinely logged in, not just that the string looks token-shaped. */
export async function verifyMakerworldCookie(rawCookieOrToken: string): Promise<boolean> {
export async function verifyMakerworldCookie(rawCookieOrToken: string): Promise<MakerworldCookieCheck> {
const bearerToken = extractMakerworldBearerToken(rawCookieOrToken);
if (!bearerToken) return false;
if (!bearerToken) return { result: "invalid" };
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), CLOUD_API_TIMEOUT_MS);
try {
@@ -123,18 +131,21 @@ export async function verifyMakerworldCookie(rawCookieOrToken: string): Promise<
redirect: "follow",
signal: controller.signal,
});
if (res.status === 403 && isFlaresolverrEnabled() && looksLikeCloudflareBlock(res.headers)) {
if (res.status === 403 && looksLikeCloudflareBlock(res.headers)) {
// A Cloudflare challenge is about this server's IP, not the token -- it says nothing
// either way about the cookie, so without a way past it the answer is "couldn't check".
if (!isFlaresolverrEnabled()) return { result: "unverifiable", reason: "cloudflare_no_flaresolverr" };
// This endpoint answers 200 with an empty body through FlareSolverr's browser when the
// session isn't actually logged in, so a bare status check would accept any string here.
// Assert on the parsed body instead -- only a genuinely authenticated session has a `uid`.
const solved = await fetchViaFlaresolverr(SELF_PREFERENCE_URL, `token=${bearerToken}`);
if (!solved) return false;
if (!solved) return { result: "unverifiable", reason: "flaresolverr_failed" };
const data = extractJsonFromBrowserBody(solved.body);
return isRecord(data) && data.uid != null;
return isRecord(data) && data.uid != null ? { result: "valid" } : { result: "invalid" };
}
return res.ok;
return res.ok ? { result: "valid" } : { result: "invalid" };
} catch {
return false;
return { result: "unverifiable", reason: "network" };
} finally {
clearTimeout(timeout);
}