-
Notifications
You must be signed in to change notification settings - Fork 4
[CDX-480] Expose headers for Autocomplete and Search Responses #187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -926,8 +926,11 @@ public String retrieveVariationsAsJson(VariationsRequest req) throws Constructor | |
| public AutocompleteResponse autocomplete(AutocompleteRequest req, UserInfo userInfo) | ||
| throws ConstructorException { | ||
| try { | ||
| String json = autocompleteAsJSON(req, userInfo); | ||
| return createAutocompleteResponse(json); | ||
| Request request = createAutocompleteRequest(req, userInfo); | ||
| Response response = clientWithRetry.newCall(request).execute(); | ||
| Map<String, List<String>> headers = response.headers().toMultimap(); | ||
|
Mudaafi marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Important Issue: Headers are read from the Alternatively, just swap the lines so String json = getResponseBody(response); // closes response, throws on error
Map<String, List<String>> headers = response.headers().toMultimap(); // still safe post-close
return createSearchResponse(json, headers);OkHttp |
||
| String json = getResponseBody(response); | ||
| return createAutocompleteResponse(json, headers); | ||
| } catch (Exception exception) { | ||
| throw new ConstructorException(exception); | ||
| } | ||
|
|
@@ -947,6 +950,25 @@ public AutocompleteResponse autocomplete(AutocompleteRequest req, UserInfo userI | |
| */ | ||
| public String autocompleteAsJSON(AutocompleteRequest req, UserInfo userInfo) | ||
| throws ConstructorException { | ||
| try { | ||
| Request request = createAutocompleteRequest(req, userInfo); | ||
| Response response = clientWithRetry.newCall(request).execute(); | ||
| return getResponseBody(response); | ||
| } catch (Exception exception) { | ||
| throw new ConstructorException(exception); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates an autocomplete OkHttp request | ||
| * | ||
| * @param req the autocomplete request | ||
| * @param userInfo optional information about the user | ||
| * @return an autocomplete OkHttp request | ||
| * @throws ConstructorException | ||
| */ | ||
| protected Request createAutocompleteRequest(AutocompleteRequest req, UserInfo userInfo) | ||
|
Mudaafi marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: The Javadoc |
||
| throws ConstructorException { | ||
| try { | ||
| List<String> paths = Arrays.asList("autocomplete", req.getQuery()); | ||
| HttpUrl url = (userInfo == null) ? this.makeUrl(paths) : this.makeUrl(paths, userInfo); | ||
|
|
@@ -1009,10 +1031,7 @@ public String autocompleteAsJSON(AutocompleteRequest req, UserInfo userInfo) | |
| .build(); | ||
| } | ||
|
|
||
| Request request = this.makeUserRequestBuilder(userInfo).url(url).get().build(); | ||
|
|
||
| Response response = clientWithRetry.newCall(request).execute(); | ||
| return getResponseBody(response); | ||
| return this.makeUserRequestBuilder(userInfo).url(url).get().build(); | ||
| } catch (Exception exception) { | ||
| throw new ConstructorException(exception); | ||
| } | ||
|
|
@@ -1171,8 +1190,9 @@ public SearchResponse search(SearchRequest req, UserInfo userInfo) throws Constr | |
| try { | ||
| Request request = createSearchRequest(req, userInfo); | ||
| Response response = clientWithRetry.newCall(request).execute(); | ||
| Map<String, List<String>> headers = response.headers().toMultimap(); | ||
| String json = getResponseBody(response); | ||
| return createSearchResponse(json); | ||
| return createSearchResponse(json, headers); | ||
| } catch (Exception exception) { | ||
| throw new ConstructorException(exception); | ||
| } | ||
|
|
@@ -1207,8 +1227,10 @@ public void onFailure(Call call, IOException e) { | |
| public void onResponse(Call call, final Response response) | ||
| throws IOException { | ||
| try { | ||
| Map<String, List<String>> headers = | ||
| response.headers().toMultimap(); | ||
| String json = getResponseBody(response); | ||
| SearchResponse res = createSearchResponse(json); | ||
| SearchResponse res = createSearchResponse(json, headers); | ||
| c.onResponse(res); | ||
| } catch (Exception e) { | ||
| c.onFailure(new ConstructorException(e)); | ||
|
|
@@ -2200,6 +2222,11 @@ protected String getVersion() { | |
| * to do it in a Gson Type Adapter. | ||
| */ | ||
| protected static AutocompleteResponse createAutocompleteResponse(String string) { | ||
| return createAutocompleteResponse(string, null); | ||
| } | ||
|
|
||
| protected static AutocompleteResponse createAutocompleteResponse( | ||
|
constructor-claude-bedrock[bot] marked this conversation as resolved.
|
||
| String string, Map<String, List<String>> headers) { | ||
| JSONObject json = new JSONObject(string); | ||
| JSONObject sections = json.getJSONObject("sections"); | ||
| for (Object sectionKey : sections.keySet()) { | ||
|
|
@@ -2208,7 +2235,11 @@ protected static AutocompleteResponse createAutocompleteResponse(String string) | |
| moveMetadataOutOfResultData(results); | ||
| } | ||
| String transformed = json.toString(); | ||
| return new Gson().fromJson(transformed, AutocompleteResponse.class); | ||
| AutocompleteResponse autocompleteResponse = | ||
| new Gson().fromJson(transformed, AutocompleteResponse.class); | ||
| autocompleteResponse.setHeaders(headers); | ||
|
|
||
| return autocompleteResponse; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -2217,6 +2248,11 @@ protected static AutocompleteResponse createAutocompleteResponse(String string) | |
| * in a Gson Type Adapter. | ||
| */ | ||
| protected static SearchResponse createSearchResponse(String string) { | ||
| return createSearchResponse(string, null); | ||
| } | ||
|
|
||
| protected static SearchResponse createSearchResponse( | ||
| String string, Map<String, List<String>> headers) { | ||
| JSONObject json = new JSONObject(string); | ||
| JSONObject response = json.getJSONObject("response"); | ||
| JSONArray results; | ||
|
|
@@ -2227,7 +2263,9 @@ protected static SearchResponse createSearchResponse(String string) { | |
| } | ||
|
|
||
| String transformed = json.toString(); | ||
| return new Gson().fromJson(transformed, SearchResponse.class); | ||
| SearchResponse searchResponse = new Gson().fromJson(transformed, SearchResponse.class); | ||
| searchResponse.setHeaders(headers); | ||
| return searchResponse; | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package io.constructor.client.models; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** Base class that provides access to HTTP response headers. */ | ||
| public abstract class ResponseHeaders { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: The // transient: excluded from Gson deserialization (headers are set manually after HTTP response)
private transient Map<String, List<String>> headers = Collections.emptyMap();Also, the |
||
|
|
||
| private transient Map<String, List<String>> headers = | ||
| Collections.<String, List<String>>emptyMap(); | ||
|
|
||
| /** | ||
| * @return the HTTP response headers | ||
| */ | ||
| public Map<String, List<String>> getHeaders() { | ||
| return headers; | ||
| } | ||
|
|
||
| /** | ||
| * @param headers the HTTP response headers to set. If null, defaults to an empty map. | ||
| */ | ||
| public void setHeaders(Map<String, List<String>> headers) { | ||
| this.headers = (headers != null) ? headers : Collections.<String, List<String>>emptyMap(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,11 @@ | ||
| package io.constructor.client; | ||
|
|
||
| import static org.junit.Assert.assertEquals; | ||
| import static org.junit.Assert.assertNotNull; | ||
|
|
||
| import io.constructor.client.models.AutocompleteResponse; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import okhttp3.mockwebserver.MockResponse; | ||
| import okhttp3.mockwebserver.MockWebServer; | ||
| import okhttp3.mockwebserver.RecordedRequest; | ||
|
|
@@ -41,8 +45,7 @@ public void AutocompleteWithPlusShouldBeEncodedInUrl() throws Exception { | |
| constructor.autocomplete(request, null); | ||
|
|
||
| RecordedRequest recordedRequest = mockServer.takeRequest(); | ||
| String expectedPath = | ||
| String.format("/autocomplete/r%%2Bco?key=%s&c=ciojava-7.5.0", apiKey); | ||
| String expectedPath = String.format("/autocomplete/r%%2Bco?key=%s&c=ciojava-7.5.0", apiKey); | ||
| String actualPath = recordedRequest.getPath(); | ||
| assertEquals("recorded request is encoded correctly", actualPath, expectedPath); | ||
| } | ||
|
|
@@ -59,8 +62,7 @@ public void AutocompleteWithSpaceShouldBeEncodedInUrl() throws Exception { | |
| constructor.autocomplete(request, null); | ||
|
|
||
| RecordedRequest recordedRequest = mockServer.takeRequest(); | ||
| String expectedPath = | ||
| String.format("/autocomplete/r%%20co?key=%s&c=ciojava-7.5.0", apiKey); | ||
| String expectedPath = String.format("/autocomplete/r%%20co?key=%s&c=ciojava-7.5.0", apiKey); | ||
| String actualPath = recordedRequest.getPath(); | ||
| assertEquals("recorded request is encoded correctly", actualPath, expectedPath); | ||
| } | ||
|
|
@@ -77,8 +79,7 @@ public void AutocompleteWithSlashShouldBeEncodedInUrl() throws Exception { | |
| constructor.autocomplete(request, null); | ||
|
|
||
| RecordedRequest recordedRequest = mockServer.takeRequest(); | ||
| String expectedPath = | ||
| String.format("/autocomplete/r%%2Fco?key=%s&c=ciojava-7.5.0", apiKey); | ||
| String expectedPath = String.format("/autocomplete/r%%2Fco?key=%s&c=ciojava-7.5.0", apiKey); | ||
| String actualPath = recordedRequest.getPath(); | ||
| assertEquals("recorded request is encoded correctly", actualPath, expectedPath); | ||
| } | ||
|
|
@@ -99,4 +100,32 @@ public void AutocompleteWithSingleQuoteShouldBeAllowedInUrl() throws Exception { | |
| String actualPath = recordedRequest.getPath(); | ||
| assertEquals("recorded request is encoded correctly", actualPath, expectedPath); | ||
| } | ||
|
|
||
| @Test | ||
| public void AutocompleteShouldReturnRateLimitHeaders() throws Exception { | ||
| String string = Utils.getTestResource("response.autocomplete.peanut.json"); | ||
| MockResponse mockResponse = | ||
| new MockResponse() | ||
| .setResponseCode(200) | ||
| .setBody(string) | ||
| .addHeader("X-RateLimit-Limit", "100") | ||
| .addHeader("X-RateLimit-Remaining", "99") | ||
| .addHeader("X-RateLimit-Reset", "1620000000"); | ||
| mockServer.enqueue(mockResponse); | ||
|
|
||
| ConstructorIO constructor = | ||
| new ConstructorIO("", apiKey, false, "127.0.0.1", mockServer.getPort()); | ||
| AutocompleteRequest request = new AutocompleteRequest("peanut"); | ||
| AutocompleteResponse response = constructor.autocomplete(request, null); | ||
|
|
||
| mockServer.takeRequest(); | ||
|
constructor-claude-bedrock[bot] marked this conversation as resolved.
Mudaafi marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: |
||
|
|
||
| Map<String, List<String>> headers = response.getHeaders(); | ||
| assertNotNull("headers should not be null", headers); | ||
| assertEquals("rate limit header", "100", headers.get("x-ratelimit-limit").get(0)); | ||
|
constructor-claude-bedrock[bot] marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Important Issue: The test calls assertNotNull("x-ratelimit-limit header present", headers.get("x-ratelimit-limit"));
assertEquals("rate limit header", "100", headers.get("x-ratelimit-limit").get(0));The same applies to the |
||
| assertEquals( | ||
| "rate limit remaining header", "99", headers.get("x-ratelimit-remaining").get(0)); | ||
| assertEquals( | ||
| "rate limit reset header", "1620000000", headers.get("x-ratelimit-reset").get(0)); | ||
|
Mudaafi marked this conversation as resolved.
|
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure where to put this comment but I think we should also expose these headers on 429. I might be missing something but I think right now we throw a ConstructorException for those without exposing the rate limit headers. Wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oooh, hmm you make a good point. I'm not sure if the Exception we throw exposes anything ngl, but I agree it should be present there so that if you do get a 429, you can adjust accordingly (impt for configuration endpoints)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'm not too worried about the rest of the errors but the rate limiting headers are important specifically for 429