Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions lib/websafe/fetch-easyeda-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,14 @@ export async function fetchEasyEDAComponent(
})

if (!searchResponse.ok) {
throw new Error("Failed to search for the component")
if (searchResponse.status === 403) {
throw new Error(
`EasyEDA API rate limit exceeded while searching for "${jlcpcbPartNumber}" (HTTP 403). EasyEDA rate-limits by IP — the part number is likely fine. Wait ~2 minutes and try again.`,
)
}
throw new Error(
`Failed to search for the component (HTTP ${searchResponse.status})`,
)
}

const searchResult = await searchResponse.json()
Expand All @@ -133,7 +140,14 @@ export async function fetchEasyEDAComponent(
})

if (!componentResponse.ok) {
throw new Error("Failed to fetch the component details")
if (componentResponse.status === 403) {
throw new Error(
`EasyEDA API rate limit exceeded while fetching "${jlcpcbPartNumber}" (HTTP 403). EasyEDA rate-limits by IP — the part number is likely fine. Wait ~2 minutes and try again.`,
)
}
throw new Error(
`Failed to fetch the component details (HTTP ${componentResponse.status})`,
)
}

const componentResult = await componentResponse.json()
Expand Down
78 changes: 78 additions & 0 deletions tests/fetch-tests/rate-limit-403.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { expect, it } from "bun:test"
import { fetchEasyEDAComponent } from "lib/websafe/fetch-easyeda-json"

// EasyEDA's component API rate-limits by IP and answers HTTP 403 (not 429)
// once tripped — see issue #409. These tests use the injectable fetch option
// so they never touch the network.

const searchSuccessBody = (partNumber: string) =>
JSON.stringify({
success: true,
result: {
lists: {
lcsc: [
{
uuid: "test-uuid-1234",
dataStr: { head: { c_para: { "Supplier Part": partNumber } } },
},
],
},
},
})

it("surfaces a search-request HTTP 403 as rate limiting, not a component lookup failure", async () => {
const fetch403 = async () =>
new Response("<html>403 Forbidden</html>", { status: 403 })

expect(
fetchEasyEDAComponent("C11702", {
fetch: fetch403 as unknown as typeof globalThis.fetch,
}),
).rejects.toThrow(/rate limit/i)
})

it("surfaces a component-details HTTP 403 as rate limiting", async () => {
let callCount = 0
const fetchSearchOkThen403 = async () => {
callCount += 1
if (callCount === 1) {
return new Response(searchSuccessBody("C11702"), {
status: 200,
headers: { "content-type": "application/json" },
})
}
return new Response("<html>403 Forbidden</html>", { status: 403 })
}

expect(
fetchEasyEDAComponent("C11702", {
fetch: fetchSearchOkThen403 as unknown as typeof globalThis.fetch,
includeModelMetadata: false,
}),
).rejects.toThrow(/rate limit/i)
})

it("keeps 'Component not found' distinct from rate limiting", async () => {
const fetchEmptySearch = async () =>
new Response(
JSON.stringify({ success: true, result: { lists: { lcsc: [] } } }),
{ status: 200, headers: { "content-type": "application/json" } },
)

expect(
fetchEasyEDAComponent("C999999999", {
fetch: fetchEmptySearch as unknown as typeof globalThis.fetch,
}),
).rejects.toThrow("Component not found")
})

it("includes the HTTP status in non-403 search failures", async () => {
const fetch500 = async () =>
new Response("Internal Server Error", { status: 500 })

expect(
fetchEasyEDAComponent("C11702", {
fetch: fetch500 as unknown as typeof globalThis.fetch,
}),
).rejects.toThrow(/HTTP 500/)
})
Loading