Skip to content

Commit 7913f2e

Browse files
jvmvikclaude
andcommitted
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 <noreply@anthropic.com>
1 parent c77a5ba commit 7913f2e

2 files changed

Lines changed: 104 additions & 3 deletions

File tree

src/main/java/serpapi/SerpApi.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,12 @@ public JsonObject search(Map<String, String> parameter) throws SerpApiException
8181
* @throws SerpApiException wraps backend error message
8282
*/
8383
public JsonArray location(Map<String, String> parameter) throws SerpApiException {
84-
String content = get("/locations.json", "json", parameter);
84+
String content = get("/locations.json", "json", parameter);
8585
JsonElement element = gson.fromJson(content, JsonElement.class);
86+
// An error is reported as an object, where a successful call returns an array.
87+
if (element.isJsonObject() && element.getAsJsonObject().has("error")) {
88+
this.client.triggerSerpApiException(content);
89+
}
8690
return element.getAsJsonArray();
8791
}
8892

@@ -125,9 +129,15 @@ public JsonObject account() throws SerpApiException {
125129
* @return JsonObject created by gson parser
126130
*/
127131
private JsonObject json(String endpoint, Map<String, String> parameter) throws SerpApiException {
128-
String content = get(endpoint, "json", parameter);
132+
String content = get(endpoint, "json", parameter);
129133
JsonElement element = gson.fromJson(content, JsonElement.class);
130-
return element.getAsJsonObject();
134+
JsonObject result = element.getAsJsonObject();
135+
// SerpApi reports some failures in the body of an HTTP 200 response, so the
136+
// status code alone is not enough to tell success from failure.
137+
if (result.has("error")) {
138+
this.client.triggerSerpApiException(content);
139+
}
140+
return result;
131141
}
132142

133143
/***
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package serpapi;
2+
3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonObject;
5+
import org.junit.Test;
6+
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
10+
import static org.junit.Assert.*;
11+
12+
/**
13+
* Test that an error reported in the body of an HTTP 200 response is raised as
14+
* a SerpApiException rather than handed back to the caller as a result.
15+
*/
16+
public class ErrorResponseTest {
17+
18+
/**
19+
* Stubbed HTTP client returning a canned body, so these tests never reach the network.
20+
*/
21+
private static class StubHttp extends SerpApiHttp {
22+
private final String body;
23+
24+
StubHttp(String body) {
25+
super("/search");
26+
this.body = body;
27+
}
28+
29+
@Override
30+
public String get(Map<String, String> parameter) {
31+
return body;
32+
}
33+
}
34+
35+
// Recorded from the google_events engine: HTTP 200, status "Success", no results.
36+
private static final String EMPTY_EVENTS = "{"
37+
+ "\"search_metadata\":{\"status\":\"Success\"},"
38+
+ "\"search_information\":{\"events_results_state\":\"Fully empty\"},"
39+
+ "\"error\":\"Google hasn't returned any results for this query.\"}";
40+
41+
private static final String ORGANIC_RESULTS =
42+
"{\"search_metadata\":{\"status\":\"Success\"},\"organic_results\":[{\"position\":1}]}";
43+
44+
private static SerpApi clientReturning(String body) {
45+
SerpApi serpapi = new SerpApi(new HashMap<>());
46+
serpapi.client = new StubHttp(body);
47+
return serpapi;
48+
}
49+
50+
@Test
51+
public void searchRaisesOnErrorInBody() {
52+
try {
53+
clientReturning(EMPTY_EVENTS).search(new HashMap<>());
54+
fail("expected SerpApiException for a 200 response carrying an error field");
55+
} catch (SerpApiException e) {
56+
assertEquals("Google hasn't returned any results for this query.", e.getMessage());
57+
}
58+
}
59+
60+
@Test
61+
public void searchReturnsResultsWhenBodyHasNoError() throws SerpApiException {
62+
JsonObject results = clientReturning(ORGANIC_RESULTS).search(new HashMap<>());
63+
assertEquals(1, results.getAsJsonArray("organic_results").size());
64+
}
65+
66+
@Test
67+
public void accountRaisesOnErrorInBody() {
68+
try {
69+
clientReturning("{\"error\":\"Invalid API key.\"}").account();
70+
fail("expected SerpApiException for a 200 response carrying an error field");
71+
} catch (SerpApiException e) {
72+
assertEquals("Invalid API key.", e.getMessage());
73+
}
74+
}
75+
76+
@Test
77+
public void locationRaisesOnErrorInBody() {
78+
try {
79+
clientReturning("{\"error\":\"Invalid API key.\"}").location(new HashMap<>());
80+
fail("expected SerpApiException instead of a cast failure on the error object");
81+
} catch (SerpApiException e) {
82+
assertEquals("Invalid API key.", e.getMessage());
83+
}
84+
}
85+
86+
@Test
87+
public void locationReturnsArrayWhenBodyHasNoError() throws SerpApiException {
88+
JsonArray locations = clientReturning("[{\"id\":\"austin\"}]").location(new HashMap<>());
89+
assertEquals(1, locations.size());
90+
}
91+
}

0 commit comments

Comments
 (0)