diff --git a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/V4CanonicalRequest.java b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/V4CanonicalRequest.java index fb6d06f7009c..54b06813090d 100644 --- a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/V4CanonicalRequest.java +++ b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/V4CanonicalRequest.java @@ -181,7 +181,10 @@ public static List>> getCanonicalHeaders(Map>> canonicalHeaders) { - StringBuilder result = new StringBuilder(512); + // 2048 chosen experimentally to avoid always needing to resize the string builder's internal byte array. + // The minimal DynamoDB get-item request at the time of testing used ~1100 bytes. 2048 was chosen as the + // next-highest power-of-two. + StringBuilder result = new StringBuilder(2048); canonicalHeaders.forEach(header -> { result.append(header.left()); result.append(":"); @@ -246,35 +249,45 @@ private static String getCanonicalRequestString(String httpMethod, String canoni * Matcher object as well. */ private static void addAndTrim(StringBuilder result, String value) { - int lengthBefore = result.length(); - boolean isStart = true; - boolean previousIsWhiteSpace = false; - - for (int i = 0; i < value.length(); i++) { - char ch = value.charAt(i); - if (isWhiteSpace(ch)) { - if (previousIsWhiteSpace || isStart) { - continue; - } - result.append(' '); - previousIsWhiteSpace = true; - } else { - result.append(ch); - isStart = false; - previousIsWhiteSpace = false; - } + int valueLength = value.length(); + if (valueLength == 0) { + return; } - if (lengthBefore == result.length()) { - return; + int start = 0; + // Find first non-whitespace + while (isWhiteSpace(value.charAt(start))) { + ++start; + if (start >= valueLength) { + return; + } } - int lastNonWhitespaceChar = result.length() - 1; - while (isWhiteSpace(result.charAt(lastNonWhitespaceChar))) { - --lastNonWhitespaceChar; + // Add things word-by-word + int lastWordStart = start; + boolean lastWasWhitespace = false; + for (int i = start; i < valueLength; i++) { + char c = value.charAt(i); + + if (isWhiteSpace(c)) { + if (!lastWasWhitespace) { + // End of word, add word + result.append(value, lastWordStart, i); + lastWasWhitespace = true; + } + } else { + if (lastWasWhitespace) { + // Start of new word, add space + result.append(' '); + lastWordStart = i; + lastWasWhitespace = false; + } + } } - result.setLength(lastNonWhitespaceChar + 1); + if (!lastWasWhitespace) { + result.append(value, lastWordStart, valueLength); + } } /** @@ -365,7 +378,17 @@ private static String getCanonicalQueryString(SortedMap> ca } private static boolean isWhiteSpace(char ch) { - return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\u000b' || ch == '\r' || ch == '\f'; + switch (ch) { + case ' ': + case '\t': + case '\n': + case '\u000b': + case '\r': + case '\f': + return true; + default: + return false; + } } /** diff --git a/core/http-auth-aws/src/test/java/software/amazon/awssdk/http/auth/aws/internal/signer/V4CanonicalRequestTest.java b/core/http-auth-aws/src/test/java/software/amazon/awssdk/http/auth/aws/internal/signer/V4CanonicalRequestTest.java index 2b24a8dfca25..66aa6abc3d5c 100644 --- a/core/http-auth-aws/src/test/java/software/amazon/awssdk/http/auth/aws/internal/signer/V4CanonicalRequestTest.java +++ b/core/http-auth-aws/src/test/java/software/amazon/awssdk/http/auth/aws/internal/signer/V4CanonicalRequestTest.java @@ -123,6 +123,36 @@ public void canonicalRequest_withSpacedHeaders_shouldStripWhitespace() { assertEquals("PUT\n/\n\nfoo:bar baz\n\nfoo\nsha-256", cr.getCanonicalRequestString()); } + @Test + public void canonicalRequest_withEmptyHeaders_shouldSucceed() { + SdkHttpRequest request = SdkHttpRequest.builder() + .protocol("https") + .host("localhost") + .method(SdkHttpMethod.PUT) + .putHeader("foo", "") + .build(); + V4CanonicalRequest cr = new V4CanonicalRequest(request, "sha-256", + new V4CanonicalRequest.Options(true, + true)); + + assertEquals("PUT\n/\n\nfoo:\n\nfoo\nsha-256", cr.getCanonicalRequestString()); + } + + @Test + public void canonicalRequest_withWhitespaceHeaders_shouldSucceed() { + SdkHttpRequest request = SdkHttpRequest.builder() + .protocol("https") + .host("localhost") + .method(SdkHttpMethod.PUT) + .putHeader("foo", " ") + .build(); + V4CanonicalRequest cr = new V4CanonicalRequest(request, "sha-256", + new V4CanonicalRequest.Options(true, + true)); + + assertEquals("PUT\n/\n\nfoo:\n\nfoo\nsha-256", cr.getCanonicalRequestString()); + } + @Test public void canonicalRequest_WithNullParamValue_shouldIncludeEquals() { SdkHttpRequest request = SdkHttpRequest.builder()