From 253a3f2415ca5f75bf6aa5e687bf061b11d9670a Mon Sep 17 00:00:00 2001 From: Victor Benarbia Date: Sat, 15 Aug 2026 22:04:19 -0500 Subject: [PATCH] fix: raise SerpApiException on an error in a 200 response body SerpApi reports some failures inside the body of an HTTP 200 response, for example the google_events engine: HTTP 200 {"search_metadata":{"status":"Success"}, "search_information":{"events_results_state":"Fully empty"}, "error":"Google hasn't returned any results for this query."} The client decided success purely from the status code, so search() returned an object that was semantically an error. Callers then reached for the key they expected and got a NullPointerException pointing at their own code, with the explanation sitting unread in the error field. This is what broke GoogleEventsTest. Check for a body-level error in json() and location(), routing it through the existing triggerSerpApiException so every SerpApi error reaches the caller as a SerpApiException regardless of status code. html() still returns its raw String unchecked; parsing arbitrary HTML as JSON to look for an error field is not worth the risk. Co-Authored-By: Claude Opus 5 --- src/main/java/serpapi/SerpApi.java | 16 +++- src/test/java/serpapi/ErrorResponseTest.java | 91 ++++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 src/test/java/serpapi/ErrorResponseTest.java diff --git a/src/main/java/serpapi/SerpApi.java b/src/main/java/serpapi/SerpApi.java index 9c24872..2a1cab4 100644 --- a/src/main/java/serpapi/SerpApi.java +++ b/src/main/java/serpapi/SerpApi.java @@ -81,8 +81,12 @@ public JsonObject search(Map parameter) throws SerpApiException * @throws SerpApiException wraps backend error message */ public JsonArray location(Map parameter) throws SerpApiException { - String content = get("/locations.json", "json", parameter); + String content = get("/locations.json", "json", parameter); JsonElement element = gson.fromJson(content, JsonElement.class); + // An error is reported as an object, where a successful call returns an array. + if (element.isJsonObject() && element.getAsJsonObject().has("error")) { + this.client.triggerSerpApiException(content); + } return element.getAsJsonArray(); } @@ -125,9 +129,15 @@ public JsonObject account() throws SerpApiException { * @return JsonObject created by gson parser */ private JsonObject json(String endpoint, Map parameter) throws SerpApiException { - String content = get(endpoint, "json", parameter); + String content = get(endpoint, "json", parameter); JsonElement element = gson.fromJson(content, JsonElement.class); - return element.getAsJsonObject(); + JsonObject result = element.getAsJsonObject(); + // SerpApi reports some failures in the body of an HTTP 200 response, so the + // status code alone is not enough to tell success from failure. + if (result.has("error")) { + this.client.triggerSerpApiException(content); + } + return result; } /*** diff --git a/src/test/java/serpapi/ErrorResponseTest.java b/src/test/java/serpapi/ErrorResponseTest.java new file mode 100644 index 0000000..141997b --- /dev/null +++ b/src/test/java/serpapi/ErrorResponseTest.java @@ -0,0 +1,91 @@ +package serpapi; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * Test that an error reported in the body of an HTTP 200 response is raised as + * a SerpApiException rather than handed back to the caller as a result. + */ +public class ErrorResponseTest { + + /** + * Stubbed HTTP client returning a canned body, so these tests never reach the network. + */ + private static class StubHttp extends SerpApiHttp { + private final String body; + + StubHttp(String body) { + super("/search"); + this.body = body; + } + + @Override + public String get(Map parameter) { + return body; + } + } + + // Recorded from the google_events engine: HTTP 200, status "Success", no results. + private static final String EMPTY_EVENTS = "{" + + "\"search_metadata\":{\"status\":\"Success\"}," + + "\"search_information\":{\"events_results_state\":\"Fully empty\"}," + + "\"error\":\"Google hasn't returned any results for this query.\"}"; + + private static final String ORGANIC_RESULTS = + "{\"search_metadata\":{\"status\":\"Success\"},\"organic_results\":[{\"position\":1}]}"; + + private static SerpApi clientReturning(String body) { + SerpApi serpapi = new SerpApi(new HashMap<>()); + serpapi.client = new StubHttp(body); + return serpapi; + } + + @Test + public void searchRaisesOnErrorInBody() { + try { + clientReturning(EMPTY_EVENTS).search(new HashMap<>()); + fail("expected SerpApiException for a 200 response carrying an error field"); + } catch (SerpApiException e) { + assertEquals("Google hasn't returned any results for this query.", e.getMessage()); + } + } + + @Test + public void searchReturnsResultsWhenBodyHasNoError() throws SerpApiException { + JsonObject results = clientReturning(ORGANIC_RESULTS).search(new HashMap<>()); + assertEquals(1, results.getAsJsonArray("organic_results").size()); + } + + @Test + public void accountRaisesOnErrorInBody() { + try { + clientReturning("{\"error\":\"Invalid API key.\"}").account(); + fail("expected SerpApiException for a 200 response carrying an error field"); + } catch (SerpApiException e) { + assertEquals("Invalid API key.", e.getMessage()); + } + } + + @Test + public void locationRaisesOnErrorInBody() { + try { + clientReturning("{\"error\":\"Invalid API key.\"}").location(new HashMap<>()); + fail("expected SerpApiException instead of a cast failure on the error object"); + } catch (SerpApiException e) { + assertEquals("Invalid API key.", e.getMessage()); + } + } + + @Test + public void locationReturnsArrayWhenBodyHasNoError() throws SerpApiException { + JsonArray locations = clientReturning("[{\"id\":\"austin\"}]").location(new HashMap<>()); + assertEquals(1, locations.size()); + } +}