fix: clone sources blocked by content-type check and TLS fingerprint
CI / check (push) Successful in 28s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m0s

- fetchSourceFurnidata now parses JSON regardless of content-type,
  so GitHub raw mirrors (Kyzegs, sphynxkitten) that serve JSON as
  text/plain work again instead of forcing a FlareSolverr round-trip.
- Add curl subprocess fallback (curl-fetch.ts) for CDNs that block
  Node's fetch by TLS fingerprint (Leet.city) — used for furnidata,
  nitro bundle and icon downloads before giving up.
- Merge verified default clone source presets with stored user sources
  so the admin always offers many working sources.
This commit is contained in:
openhands committed 2026-08-19 19:39:32 +02:00
1 parent 7911a25ced
commit 121dda7249
4 files changed
+173 -18

No files matched your search

+25 -1
View File
@@ -13,6 +13,8 @@ const {
fsUnlink,
fsReadFile,
fetchWithFlareSolver,
curlFetchText,
curlDownload,
} = vi.hoisted(() => ({
downloadFile: vi.fn<AnyFn>(async () => ({ ok: true, size: 200 })),
appendFurniEntry: vi.fn<AnyFn>(async () => {}),
@@ -22,9 +24,15 @@ const {
fsUnlink: vi.fn<AnyFn>(async () => {}),
fsReadFile: vi.fn<AnyFn>(async () => Buffer.from("NITRO")),
fetchWithFlareSolver: vi.fn<AnyFn>(async () => ""),
curlFetchText: vi.fn<AnyFn>(async () => ""),
curlDownload: vi.fn<AnyFn>(async () => ({ ok: true, size: 200 })),
}));
vi.mock("@/lib/services/import/core/download", () => ({ downloadFile }));
vi.mock("@/lib/services/import/core/curl-fetch", () => ({
curlDownload,
curlFetchText,
}));
vi.mock("@/lib/services/flare-solver", () => ({ fetchWithFlareSolver }));
vi.mock("@/lib/services/furni-data", () => ({ appendFurniEntry }));
vi.mock("@/lib/services/furni-import", () => ({
@@ -247,7 +255,7 @@ describe("fetchSourceFurnidata", () => {
expect(fetchWithFlareSolver).toHaveBeenCalledWith("https://b.test/fd.json");
});
it("falls back to FlareSolverr on non-JSON content-type even when status is 200", async () => {
it("falls back to FlareSolverr on HTML body even when status is 200", async () => {
vi.stubGlobal(
"fetch",
vi.fn(
@@ -263,6 +271,22 @@ describe("fetchSourceFurnidata", () => {
expect(list.map((e) => e.classname)).toEqual(["bc_sofa", "wall_x"]);
});
it("parses JSON served with text/plain content-type directly (e.g. GitHub raw)", async () => {
vi.stubGlobal(
"fetch",
vi.fn(
async () =>
new Response(JSON_BODY, {
status: 200,
headers: { "content-type": "text/plain; charset=utf-8" },
}),
),
);
const list = await fetchSourceFurnidata("https://g.test/fd.json", 1);
expect(list.map((e) => e.classname)).toEqual(["bc_sofa", "wall_x"]);
expect(fetchWithFlareSolver).not.toHaveBeenCalled();
});
it("throws when HTML has no embedded JSON payload", async () => {
vi.stubGlobal(
"fetch",