-
Notifications
You must be signed in to change notification settings - Fork 909
feature 2034: SdkHttpFullRequest builder.URI conditionally accept que… #2082
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
feature 2034: SdkHttpFullRequest builder.URI conditionally accept que… #2082
Conversation
I'm unsure as to why the CI failed, is there a way I can retrigger? |
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.
Thank you for the PR!
http-client-spi/src/main/java/software/amazon/awssdk/http/DefaultSdkHttpFullRequest.java
Outdated
Show resolved
Hide resolved
.map(s -> s.split("=", 2)) | ||
.collect(groupingBy(a -> a[0], mapping(a -> a[1], toList()))) | ||
.forEach((paramKey, paramValues) -> paramValues | ||
.forEach(paramValue -> this.appendUriQueryParameter(paramKey, paramValue))); |
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.
I don't think appending would be the correct behavior here; the current set of query parameters should be cleared; otherwise something like this
builder.uri(myUri1).uri(myUri2);
will contain the query parameters from both URIs.
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.
Done
http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpRequest.java
Outdated
Show resolved
Hide resolved
Hmm it's probably easier to reset your branch, something like:
|
BTW, I suspect what happened was that the when attempting to squash the commits, the merge commit was included in the commit range |
@dagnir I understand this leaves me with a local reset-branch including the latest changes from upstream (as I have origin set to my fork). It's unclear to me how I can then make sure to force push this to the httpclientspi-uri-with-query-params branch on my origin? |
9d6fae0
to
7dc5615
Compare
@dagnir I've temporary deleted my local httpclientspi-uri-with-query-params branch, checkout -b from the reset-branch to a new local httpclientspi-uri-with-query-params and then force pushed that. |
You can use a refspec to push between your branches
|
46ee998
to
ee07422
Compare
.host(uri.getHost()) | ||
.port(uri.getPort()) | ||
.encodedPath(SdkHttpUtils.appendUri(uri.getRawPath(), encodedPath())); | ||
.host(uri.getHost()) | ||
.port(uri.getPort()) | ||
.encodedPath(SdkHttpUtils.appendUri(uri.getRawPath(), encodedPath())); | ||
} |
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.
Please revert formatting change here
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.
will do
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.
Done
return this.protocol(uri.getScheme()) | ||
.host(uri.getHost()) | ||
.port(uri.getPort()) | ||
.encodedPath(SdkHttpUtils.appendUri(uri.getRawPath(), encodedPath())); | ||
.host(uri.getHost()) | ||
.port(uri.getPort()) | ||
.encodedPath(SdkHttpUtils.appendUri(uri.getRawPath(), encodedPath())); | ||
} |
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.
Please revert formatting change
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.
Done
Pattern.compile("\\s*&|%26\\s*") | ||
.splitAsStream(uri.getQuery().trim()) | ||
.map(s -> s.split("=|%3D", 2)) | ||
.collect(groupingBy(a -> a[0], mapping(a -> a[1], toList()))) | ||
.forEach((paramKey, paramValues) -> paramValues | ||
.forEach(paramValue -> this.appendUriQueryParameter(paramKey, paramValue))); |
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.
This code is pretty complex and is duplicated here and in SdkHttpRequest
. Let's move this out into SdkHttpUtils
instead. Should also make the parsing easier to test in isolation
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.
Agreed
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.
extracted
default Builder uriWithQueryParams(URI uri) { | ||
Builder builder = this.uri(uri); | ||
if (uri.getQuery() != null) { | ||
Pattern.compile("\\s*&|%26\\s*") |
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.
- I don't think we should including
%26
here as the literal&
should be the only delimiter we look for. - Compiling a pattern each time is inefficient. Once this logic is moved into
SdkHttpUtils
, let's extract the pattern out into a static constant
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.
In appendUriQueryParameter impl I now decode the incoming paramValue as was suggested before, which is why I reasoned to also split on the decoded delimiter as otherwise the value would not be split properly before appending?
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.
Unfortunately I don't think that's the right strategy. We should do the decoding after we have the individual components of the query parsed out. Otherwise, if the the input URI
has and encoded &
or =
, then we would be parsing it incorrectly. It's perfectly valid for the parameter name or value to have &
or =
it as long as it's encoded.
For example, the pair [contains=equals, foo] in a URI would be https://foo.bar?contains%3Dequals=foo
, but the code as is will split this as [contains, equals=foo]
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.
I think the general strategy should be
- get components of query string
- foreach component
- decode name
- decode value if present
- put raw query param
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.
I haven't thought about it in that way, makes sense to split like that and then decode both parts before adding.
On reusing the put raw query param bullet, that would mean we cannot differ on where te params come from as I did now in:
private Map<String, List<String>> determineQueryParameters(Builder builder) {
if (builder.uriQueryParameters.isEmpty()) {
return builder.queryParametersAreFromToBuilder
? builder.queryParameters
: deepUnmodifiableMap(builder.queryParameters, () -> new LinkedHashMap<>());
}
return builder.uriQueryParameters;
}
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.
Hmm I don't think it should be necessary to distinguish the source. uriWithQueryParams
acts basically like a static constructor for SdkHttpRequest
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.
Attempted to reflect this train of thought
if (uri.getQuery() != null) { | ||
Pattern.compile("\\s*&|%26\\s*") | ||
.splitAsStream(uri.getQuery().trim()) | ||
.map(s -> s.split("=|%3D", 2)) |
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.
Same here, should only splitting on the literal =
.
Also seems like this assumes each query parameter is a name-value pair; we should support parameters that don't have a value as well, like in https://github.com/aws/aws-sdk-java-v2/pull/2082?foo
* @param paramName The name of the query parameter to add | ||
* @param paramValue The un-encoded value for the query parameter. | ||
*/ | ||
Builder appendUriQueryParameter(String paramName, String paramValue); |
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.
Hmm why do we need this instead of using putRawQueryParameter
?
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.
since I wanted to make sure not to interfere with already in place param handling
@@ -0,0 +1,5 @@ | |||
{ | |||
"category": "HTTP client spi", |
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.
Please capitalize here to HTTP Client SPI
to be consistent with the rest of the log entries
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.
I made an educated guess before so was unaware of the casing significance
c42de89
to
f0d18f9
Compare
*/ | ||
public static Map<String, List<String>> uriParams(URI uri) { | ||
return Pattern.compile(QUERY_PARAM_PATTERN) | ||
.splitAsStream(uri.getQuery().trim()) |
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.
BTW, I think we should be looking at getRawQuery()
here, since getQuery()
will give you the decoded form which is not what we want
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.
You're right, updated and adapted unit test
URI uri = new URI("https://github.com/aws/aws-sdk-java-v2/issues/2034?reqParam=1234&oParam=3456%26reqParam%3D5678"); | ||
final SdkHttpFullRequest sdkHttpFullRequest = | ||
SdkHttpFullRequest.builder().method(SdkHttpMethod.POST).uriWithQueryParams(uri).build(); | ||
assertThat(sdkHttpFullRequest.getUri().getQuery()).contains("reqParam=1234", "oParam=3456", "reqParam=5678"); |
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.
I don't think this assertion is correct per my comment earlier re: parsing.
Splitting reqParam=1234&oParam=3456%26reqParam%3D5678
on the &
delimiter should give reqParam=1234
and oParam=3456%26reqParam%3D5678
as the two name-value pairs, then parsing each as a pair gives [reqParam, 1234] and [oParam, 3456%26reqParam%3D5678]
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.
Today I learned! updated and adapted unit test
this.uriQueryParameters.computeIfAbsent(paramName, k -> new ArrayList<>()) | ||
.add(URLDecoder.decode(paramValue, StandardCharsets.UTF_8.toString())); | ||
} catch (UnsupportedEncodingException e) { | ||
log.warn("Could not decode {}={} using {}", paramName, paramValue, StandardCharsets.UTF_8); |
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.
We should just fail instead of logging a warning here
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.
method is no longer needed
Discussed with millems@ and we think just changing the |
4cd0e2d
to
a5f5dad
Compare
I'm hoping the current version is aligned with the expectations |
Codecov Report
@@ Coverage Diff @@
## master #2082 +/- ##
=========================================
Coverage 77.12% 77.13%
- Complexity 298 299 +1
=========================================
Files 1204 1206 +2
Lines 37957 37924 -33
Branches 2984 2963 -21
=========================================
- Hits 29274 29251 -23
+ Misses 7241 7228 -13
- Partials 1442 1445 +3
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
* Extracts query parameters from the given URI | ||
*/ | ||
public static Map<String, List<String>> uriParams(URI uri) { | ||
return Pattern.compile(QUERY_PARAM_PATTERN) |
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.
The result of Pattern.compile()
itself should be reused, so we don't need to call compile()
each time.
private static final Pattern QUERY_PARAM_PATTERN = ...
Same for split()
below since it also uses regex compile under the hood
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.
BTW, I think a more accurate name would be "QUERY_PARAM_DELIMETER_PATTERN"
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.
the split is safe to use in my opinion since it uses a StringTokenizer if only a single character is provided since java 7 according to http://hg.openjdk.java.net/jdk7/jdk7/jdk/rev/1ff977b938e5
if (uri.getRawQuery() != null) { | ||
SdkHttpUtils.uriParams(uri) | ||
.forEach(this::putRawQueryParameter); | ||
} | ||
return builder; |
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.
The existing set of query parameters set on the builder should be cleared first
if (uri.getRawQuery() != null) { | ||
SdkHttpUtils.uriParams(uri) | ||
.forEach(this::putRawQueryParameter); | ||
} | ||
return builder; |
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.
Same here, the existing set of query parameters set on the builder should be cleared first
@@ -173,4 +178,16 @@ public void headersFromCollectionWorksCorrectly() { | |||
assertThat(SdkHttpUtils.firstMatchingHeaderFromCollection(headers, asList("foo", "nothing"))).hasValue("bar"); | |||
assertThat(SdkHttpUtils.firstMatchingHeaderFromCollection(headers, asList("foo", "other"))).hasValue("foo"); | |||
} | |||
|
|||
@Test | |||
public void uriParams() throws URISyntaxException { |
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.
We should have tests for the two requests classes that we changed as well.
public static Map<String, List<String>> uriParams(URI uri) { | ||
return Pattern.compile(QUERY_PARAM_PATTERN) | ||
.splitAsStream(uri.getRawQuery().trim()) | ||
.map(s -> s.contains("=") ? s.split("=", 2) : new String[] {s, ""}) |
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.
Hmm I don't think using an empty string when there's no =
is what we want; we should be setting null
if there's no =
for the parameter. There should be a test for the result of something like this is
URI myUri = URI.create("https://github.com/aws/aws-sdk-for-java-v2?foo");
SdkHttpFullRequest request = SdkHttpFullRequest.builder().uri(myUri).build();
request.getUri();
to make sure that the result is not https://github.com/aws/aws-sdk-for-java-v2?foo=
, which will likely be interpreted differently by the server
Map<String, List<String>> uriParams = SdkHttpUtils.uriParams(uri); | ||
assertThat(uriParams).contains(entry("reqParam", Arrays.asList("1234", "5678")), | ||
entry("oParam", Collections.singletonList("3456")), | ||
entry("noval", Arrays.asList("")), |
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.
As mentioned above, the value for noval
should be null, not a list containing ""
.
a5f5dad
to
8ef3f77
Compare
Hopeful we're getting to a common understanding of the solution 🙏 |
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.
LGTM! I'm going kick off an integ test just to make sure none of those tests were affected by this change.
819cd77
to
c91e770
Compare
|
SonarCloud Quality Gate failed.
|
I'm happy I was able to contribute and grateful for the review. I've learned a thing or two! |
…b4814c620 Pull request: release <- staging/5c7164ee-e3ea-4091-9540-da3b4814c620
Description
When calling the SdkHttpFullRequest builder using the URI default method, query parameters are now supported in the provided URI if keepUriQueryParams is set to true on the builder.\nThis can be useful in case you want to provide an already fully formed URI like a callback URI.
Motivation and Context
Query parameters that were provided to the builder in the form of the passed URI were omitted before
#2034
Testing
added test cases in SdkHttpRequestResponseTest
Screenshots (if appropriate)
Types of changes
Checklist
mvn install
succeedsLicense