Fix: don't mask DataForSEO 40501 validation errors as empty results (#346)

* Fix: don't mask DataForSEO 40501 validation errors as empty results

isNoResultsTask classified any task with status_code 40501 as a
successful empty result, but 40501 is not unique to "No Search
Results" — DataForSEO also returns it for validation rejections
like "Invalid Field: 'target'.".

After the Business Listings and Q&A endpoints opted into
treatNoResultsAsEmpty, invalid-field 40501 responses (reachable via
the MCP search_local_businesses categories input, which accepts
arbitrary strings) were silently masked as empty successes — billed
and tracked, but hiding the real provider validation error.

Match on the status message ("no search results") instead of the
ambiguous code so charged validation failures flow through to
DataforseoChargedTaskError with the diagnostic message, while genuine
no-results responses still return as empty successes. Fixes all call
sites uniformly (Business, SERP live, SERP task_get polling).

* Format isNoResultsTask per prettier
This commit is contained in:
Ben Senescu 2026-07-05 17:54:14 -04:00 committed by GitHub
parent 9b6c5640cd
commit e39de22f96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 32 additions and 3 deletions

View File

@ -148,4 +148,29 @@ describe("assertOk", () => {
), ),
).toBe(task); ).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 });
}
}
});
}); });

View File

@ -102,11 +102,15 @@ function describeInvalidField(
return `${message} (sent ${field}=${JSON.stringify(value)})`; 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 { export function isNoResultsTask(task: DataforseoTaskLike): boolean {
return ( return (
task.status_code === 40501 || task.status_message?.toLowerCase().includes("no search results") ?? false
(task.status_message?.toLowerCase().includes("no search results") ?? false)
); );
} }