diff --git a/src/server/lib/dataforseo/envelope.test.ts b/src/server/lib/dataforseo/envelope.test.ts index 1f994eb..b95998a 100644 --- a/src/server/lib/dataforseo/envelope.test.ts +++ b/src/server/lib/dataforseo/envelope.test.ts @@ -148,4 +148,29 @@ describe("assertOk", () => { ), ).toBe(task); }); + + it("still surfaces a charged 40501 'Invalid Field' failure even with treatNoResultsAsEmpty", () => { + // 40501 is not unique to no-results — it also covers validation rejections, + // which are real charged failures we must not mask as empty results. + const task = { + status_code: 40501, + status_message: "Invalid Field: 'categories'.", + path: ["v3", "business_data", "business_listings", "search", "live"], + cost: 0.02, + result_count: 0, + data: { categories: ["not_a_real_category"] }, + }; + try { + assertOk( + { status_code: 20000, tasks: [task] }, + { treatNoResultsAsEmpty: true }, + ); + throw new Error("expected assertOk to throw"); + } catch (error) { + expect(error).toBeInstanceOf(DataforseoChargedTaskError); + if (error instanceof DataforseoChargedTaskError) { + expect(error.billing).toEqual({ path: task.path, costUsd: 0.02 }); + } + } + }); }); diff --git a/src/server/lib/dataforseo/envelope.ts b/src/server/lib/dataforseo/envelope.ts index 88e1de9..ae61344 100644 --- a/src/server/lib/dataforseo/envelope.ts +++ b/src/server/lib/dataforseo/envelope.ts @@ -102,11 +102,15 @@ function describeInvalidField( return `${message} (sent ${field}=${JSON.stringify(value)})`; } -/** DataForSEO's "No Search Results" (40501) — a successful empty result, not a failure. */ +/** + * DataForSEO's "No Search Results" (40501) — a successful empty result, not a + * failure. Match on the status message, not the code alone: 40501 also covers + * validation rejections like "Invalid Field: 'target'.", which are real charged + * failures we must surface rather than mask as empty results. + */ export function isNoResultsTask(task: DataforseoTaskLike): boolean { return ( - task.status_code === 40501 || - (task.status_message?.toLowerCase().includes("no search results") ?? false) + task.status_message?.toLowerCase().includes("no search results") ?? false ); }