-
Notifications
You must be signed in to change notification settings - Fork 0
Optimize cosmos gateway header handling #28
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: main
Are you sure you want to change the base?
Changes from 6 commits
3f74a01
9ce646b
0effd86
2f74a12
0d8cea7
2d20975
ba6715f
3cafcee
63a0bde
52158fe
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 |
|---|---|---|
|
|
@@ -15,9 +15,9 @@ | |
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
|
|
||
|
|
@@ -30,8 +30,7 @@ | |
| public class StoreResponse { | ||
| private static final Logger logger = LoggerFactory.getLogger(StoreResponse.class.getSimpleName()); | ||
| final private int status; | ||
| final private String[] responseHeaderNames; | ||
| final private String[] responseHeaderValues; | ||
| final private Map<String, String> responseHeaders; | ||
| private int requestPayloadLength; | ||
| private RequestTimeline requestTimeline; | ||
| private RntbdChannelAcquisitionTimeline channelAcquisitionTimeline; | ||
|
|
@@ -56,22 +55,14 @@ public StoreResponse( | |
| checkArgument((contentStream == null) == (responsePayloadLength == 0), | ||
| "Parameter 'contentStream' must be consistent with 'responsePayloadLength'."); | ||
| requestTimeline = RequestTimeline.empty(); | ||
| responseHeaderNames = new String[headerMap.size()]; | ||
| responseHeaderValues = new String[headerMap.size()]; | ||
| this.responseHeaders = toLowerCasedMap(headerMap); | ||
| this.endpoint = endpoint != null ? endpoint : ""; | ||
|
|
||
| int i = 0; | ||
| for (Map.Entry<String, String> headerEntry : headerMap.entrySet()) { | ||
| responseHeaderNames[i] = headerEntry.getKey(); | ||
| responseHeaderValues[i] = headerEntry.getValue(); | ||
| i++; | ||
| } | ||
|
|
||
| this.status = status; | ||
| replicaStatusList = new HashMap<>(); | ||
| if (contentStream != null) { | ||
| try { | ||
| this.responsePayload = new JsonNodeStorePayload(contentStream, responsePayloadLength, headerMap); | ||
| this.responsePayload = new JsonNodeStorePayload(contentStream, responsePayloadLength, this.responseHeaders); | ||
| } finally { | ||
| try { | ||
| contentStream.close(); | ||
|
|
@@ -91,37 +82,34 @@ private StoreResponse( | |
| String endpoint, | ||
| int status, | ||
| Map<String, String> headerMap, | ||
| JsonNodeStorePayload responsePayload) { | ||
| JsonNodeStorePayload responsePayload, | ||
| boolean keysAlreadyLowerCased) { | ||
|
|
||
| checkNotNull(endpoint, "Parameter 'endpoint' must not be null."); | ||
|
|
||
| requestTimeline = RequestTimeline.empty(); | ||
| responseHeaderNames = new String[headerMap.size()]; | ||
| responseHeaderValues = new String[headerMap.size()]; | ||
| this.responseHeaders = keysAlreadyLowerCased ? headerMap : toLowerCasedMap(headerMap); | ||
| this.endpoint = endpoint; | ||
|
|
||
| int i = 0; | ||
| for (Map.Entry<String, String> headerEntry : headerMap.entrySet()) { | ||
| responseHeaderNames[i] = headerEntry.getKey(); | ||
| responseHeaderValues[i] = headerEntry.getValue(); | ||
| i++; | ||
| } | ||
|
|
||
| this.status = status; | ||
| replicaStatusList = new HashMap<>(); | ||
| this.responsePayload = responsePayload; | ||
| } | ||
|
|
||
| public int getStatus() { | ||
| return status; | ||
| private static Map<String, String> toLowerCasedMap(Map<String, String> map) { | ||
| Map<String, String> result = new HashMap<>(map.size()); | ||
| for (Map.Entry<String, String> entry : map.entrySet()) { | ||
| result.put(entry.getKey().toLowerCase(Locale.ROOT), entry.getValue()); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| public String[] getResponseHeaderNames() { | ||
| return responseHeaderNames; | ||
| public int getStatus() { | ||
| return status; | ||
| } | ||
|
|
||
| public String[] getResponseHeaderValues() { | ||
| return responseHeaderValues; | ||
| public Map<String, String> getResponseHeaders() { | ||
| return 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. 🟡 Recommendation · Code Quality:
storeResponse.getResponseHeaders().put("x-ms-foo", "injected"); // modifies StoreResponse internalsAll current production callers are well-behaved (they either take a defensive copy or read-only), but this is a latent footgun. Hardening with public Map(String, String) getResponseHeaders() {
return Collections.unmodifiableMap(responseHeaders);
} |
||
| } | ||
|
|
||
| public void setRntbdRequestLength(int rntbdRequestLength) { | ||
|
|
@@ -191,31 +179,19 @@ public String getCorrelatedActivityId() { | |
| } | ||
|
|
||
| public String getHeaderValue(String attribute) { | ||
| if (this.responseHeaderValues == null || this.responseHeaderNames.length != this.responseHeaderValues.length) { | ||
| if (this.responseHeaders == null) { | ||
| return null; | ||
| } | ||
|
|
||
| for (int i = 0; i < responseHeaderNames.length; i++) { | ||
| if (responseHeaderNames[i].equalsIgnoreCase(attribute)) { | ||
| return responseHeaderValues[i]; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| return responseHeaders.get(attribute.toLowerCase(Locale.ROOT)); | ||
| } | ||
|
|
||
| //NOTE: only used for testing purpose to change the response header value | ||
| void setHeaderValue(String headerName, String value) { | ||
| if (this.responseHeaderValues == null || this.responseHeaderNames.length != this.responseHeaderValues.length) { | ||
| if (this.responseHeaders == null) { | ||
| return; | ||
| } | ||
|
|
||
| for (int i = 0; i < responseHeaderNames.length; i++) { | ||
| if (responseHeaderNames[i].equalsIgnoreCase(headerName)) { | ||
| responseHeaderValues[i] = value; | ||
| break; | ||
| } | ||
| } | ||
| this.responseHeaders.put(headerName.toLowerCase(Locale.ROOT), value); | ||
| } | ||
|
|
||
| public double getRequestCharge() { | ||
|
|
@@ -310,23 +286,20 @@ public void setFaultInjectionRuleEvaluationResults(List<String> results) { | |
|
|
||
| public StoreResponse withRemappedStatusCode(int newStatusCode, double additionalRequestCharge) { | ||
|
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. 🟡 Recommendation · Test Coverage: This method was substantially refactored (parallel arrays →
Example skeleton: `@Test`(groups = { "unit" })
public void withRemappedStatusCode_updatesChargeAndStatus() {
HashMap(String, String) headers = new HashMap<>();
headers.put(HttpConstants.HttpHeaders.REQUEST_CHARGE.toLowerCase(Locale.ROOT), "5.0");
StoreResponse original = new StoreResponse(null, 200, headers, null, 0);
StoreResponse remapped = original.withRemappedStatusCode(201, 2.0);
assertThat(remapped.getStatus()).isEqualTo(201);
assertThat(Double.parseDouble(remapped.getHeaderValue(HttpConstants.HttpHeaders.REQUEST_CHARGE))).isEqualTo(7.0);
// original is unmodified
assertThat(original.getStatus()).isEqualTo(200);
} |
||
|
|
||
| Map<String, String> headers = new HashMap<>(); | ||
| for (int i = 0; i < this.responseHeaderNames.length; i++) { | ||
| String headerName = this.responseHeaderNames[i]; | ||
| if (headerName.equalsIgnoreCase(HttpConstants.HttpHeaders.REQUEST_CHARGE)) { | ||
| double currentRequestCharge = this.getRequestCharge(); | ||
| double newRequestCharge = currentRequestCharge + additionalRequestCharge; | ||
| headers.put(headerName, String.valueOf(newRequestCharge)); | ||
| } else { | ||
| headers.put(headerName, this.responseHeaderValues[i]); | ||
| } | ||
| Map<String, String> headers = new HashMap<>(this.responseHeaders); | ||
| String requestChargeKey = HttpConstants.HttpHeaders.REQUEST_CHARGE.toLowerCase(Locale.ROOT); | ||
| if (headers.containsKey(requestChargeKey)) { | ||
| double currentRequestCharge = this.getRequestCharge(); | ||
| double newRequestCharge = currentRequestCharge + additionalRequestCharge; | ||
| headers.put(requestChargeKey, String.valueOf(newRequestCharge)); | ||
| } | ||
|
|
||
| return new StoreResponse( | ||
| this.endpoint, | ||
| newStatusCode, | ||
| headers, | ||
| this.responsePayload); | ||
| this.responsePayload, | ||
| true); | ||
| } | ||
|
|
||
| public String getEndpoint() { | ||
|
|
||
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.
🔴 Blocking · Correctness:
HttpUtils.unescape()always throwsUnsupportedOperationExceptionon theImmutableMapreturned byasMap()response.getHeaders().asMap(activityId)returns acom.azure.cosmos.implementation.guava25.collect.ImmutableMap. This PR now passes that directly toHttpUtils.unescape():HttpUtils.unescape(Map)callsheaders.computeIfPresent(...), and the vendoredImmutableMapoverridescomputeIfPresentto always throw unconditionally:So every call through
ThinClientStoreModel.unwrapToStoreResponsethat successfully decodes anRntbdResponsewill throwUnsupportedOperationExceptionbefore producing aStoreResponse. ThinClient mode is broken for all responses.Before this PR, the old code wrapped in
HttpHeadersfirst and then calledunescapeon the mutable map fromtoLowerCaseMap():Fix: Wrap in a mutable
HashMapbefore callingunescape:This preserves the original semantics and avoids the
UnsupportedOperationException.