Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"core": {
"changeLogMessages": [
"Reduce per-request overhead in the HttpClient adapter: cheaper response header copy, reuse of the generated User-Agent string, request Uri and standard HttpMethod instances, and skipped attribute building for no-op telemetry providers."
],
"type": "patch",
"updateMinimum": false
}
}
31 changes: 30 additions & 1 deletion sdk/src/Core/Amazon.Runtime/AmazonServiceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,14 @@ public static Uri ComposeUrl(IRequest iRequest)
/// <param name="skipEncodingValidPathChars">If true the accepted path characters {/+:} are not encoded.</param>
/// <returns>Uri for the given SDK request</returns>
public static Uri ComposeUrl(IRequest internalRequest, bool skipEncodingValidPathChars)
{
return CreateRequestUri(ComposeUrlString(internalRequest));
}

/// <summary>
/// Builds the string form of the Uri that <see cref="ComposeUrl(IRequest)"/> returns.
/// </summary>
internal static string ComposeUrlString(IRequest internalRequest)
{
Uri url = internalRequest.Endpoint;
var resourcePath = internalRequest.ResourcePath;
Expand Down Expand Up @@ -514,14 +522,35 @@ public static Uri ComposeUrl(IRequest internalRequest, bool skipEncodingValidPat
? url.AbsoluteUri + parameterizedPath
: url.AbsoluteUri + "/" + parameterizedPath;

return strUri;
}

/// <summary>
/// Creates the request Uri from a string produced by <see cref="ComposeUrlString(IRequest)"/>.
/// </summary>
internal static Uri CreateRequestUri(string strUri)
{
#if NET8_0_OR_GREATER
return CreateRequestUri(strUri, AWSConfigs.DisableDangerousDisablePathAndQueryCanonicalization);
#else
return CreateRequestUri(strUri, false);
#endif
}

/// <summary>
/// Creates the request Uri from a string produced by <see cref="ComposeUrlString(IRequest)"/>, using the given
/// value of <c>AWSConfigs.DisableDangerousDisablePathAndQueryCanonicalization</c> (ignored before .NET 8).
/// </summary>
internal static Uri CreateRequestUri(string strUri, bool disableCanonicalizationFlag)
{
#if NET8_0_OR_GREATER
// The UriCreationOptions and DangerousDisablePathAndQueryCanonicalization were added in .NET 6 and allows
// us to turn off the Uri behavior of canonicalizing Uri. For example if the resource path was "foo/../bar.txt"
// the URI class will change the canonicalize path to bar.txt. This behavior of changing the Uri after the
// request has been signed will trigger a signature mismatch error. It is valid especially for S3 for the resource
// path to contain ".." segments.
var uriCreationOptions = new UriCreationOptions();
if (!AWSConfigs.DisableDangerousDisablePathAndQueryCanonicalization)
if (!disableCanonicalizationFlag)
{
uriCreationOptions.DangerousDisablePathAndQueryCanonicalization = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ public class HttpClientResponseData : IWebResponseData
HttpResponseMessageBody _response;
string[] _headerNames;
Dictionary<string, string> _headers;
HashSet<string> _headerNamesSet;
private Dictionary<string, IEventStreamHeader> _eventHeaders;

internal HttpClientResponseData(Dictionary<string, IEventStreamHeader> eventHeaders)
Expand Down Expand Up @@ -78,49 +77,51 @@ public string GetHeaderValue(string headerName)

public bool IsHeaderPresent(string headerName)
{
return _headerNamesSet.Contains(headerName);
return headerName != null && _headers.ContainsKey(headerName);
}

public string[] GetHeaderNames()
{
if (_headerNames == null && _response != null)
{
// Keys enumerate in insertion order (nothing is ever removed), which matches the
// response-headers-then-content-headers order they were copied in.
var headerNames = new string[_headers.Count];
_headers.Keys.CopyTo(headerNames, 0);
_headerNames = headerNames;
}
return _headerNames;
}

private void CopyHeaderValues(HttpResponseMessage response)
{
List<string> headerNames = new List<string>();
_headers = new Dictionary<string, string>(10, StringComparer.OrdinalIgnoreCase);

// The enumerated values are the same parsed values HttpHeaders.TryGetValues returns
// for the key, so the first value is taken directly instead of looking the key up again.
foreach (KeyValuePair<string, IEnumerable<string>> kvp in response.Headers)
{
headerNames.Add(kvp.Key);
var headerValue = GetFirstHeaderValue(response.Headers, kvp.Key);
_headers.Add(kvp.Key, headerValue);
_headers.Add(kvp.Key, GetFirstValue(kvp.Value));
}

if (response.Content != null)
{
foreach (var kvp in response.Content.Headers)
{
if (!headerNames.Contains(kvp.Key))
if (!_headers.ContainsKey(kvp.Key))
{
headerNames.Add(kvp.Key);
var headerValue = GetFirstHeaderValue(response.Content.Headers, kvp.Key);
_headers.Add(kvp.Key, headerValue);
_headers.Add(kvp.Key, GetFirstValue(kvp.Value));
}
}
}
_headerNames = headerNames.ToArray();
_headerNamesSet = new HashSet<string>(_headerNames, StringComparer.OrdinalIgnoreCase);
}

private string GetFirstHeaderValue(HttpHeaders headers, string key)
private static string GetFirstValue(IEnumerable<string> values)
{
IEnumerable<string> headerValues = null;
if (headers.TryGetValues(key, out headerValues))
return headerValues.FirstOrDefault();
if (values is string[] array)
return array.Length > 0 ? array[0] : null;

return string.Empty;
return values.FirstOrDefault();
}

public IEventStreamHeader GetEventStreamHeader(string headerName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,67 @@ public string GetCustomUserAgentComponents()
/// <returns>The final user-agent string including metrics data.</returns>
public string GenerateUserAgentWithMetrics()
{
var metricsUserAgent = GenerateMetricsUserAgent();
if (!string.IsNullOrEmpty(metricsUserAgent))
var customComponents = string.Join(" ", _userAgentCustomComponents);
if (_trackedFeatureIds.Count == 0)
{
return $"{string.Join(" ", _userAgentCustomComponents)} {metricsUserAgent}";
return customComponents;
}
return string.Concat(customComponents, " ", GenerateMetricsUserAgent());
}

/// <summary>
/// Creates an entry holding <see cref="GenerateUserAgentWithMetrics"/> and the components and features
/// (in order) it was generated from.
/// </summary>
internal GeneratedUserAgent CreateGeneratedUserAgent()
{
var components = new string[_userAgentCustomComponents.Count];
_userAgentCustomComponents.CopyTo(components);
var featureIds = new string[_trackedFeatureIds.Count];
_trackedFeatureIds.CopyTo(featureIds);
return new GeneratedUserAgent(components, featureIds, GenerateUserAgentWithMetrics());
}

/// <summary>
/// Returns true if <paramref name="cached"/> was generated from the same components and features
/// (same values in the same order) as this instance.
/// </summary>
internal bool Matches(GeneratedUserAgent cached)
{
return cached != null &&
SequenceEqual(_userAgentCustomComponents, cached.Components) &&
SequenceEqual(_trackedFeatureIds, cached.FeatureIds);
}

private static bool SequenceEqual(HashSet<string> set, string[] values)
{
if (set.Count != values.Length)
return false;

int i = 0;
foreach (var item in set)
{
if (!string.Equals(item, values[i++], System.StringComparison.Ordinal))
return false;
}
return true;
}

/// <summary>
/// Copies the custom components and tracked features of <paramref name="source"/> into this instance.
/// Equivalent to <c>AddUserAgentComponent(source.GetCustomUserAgentComponents())</c> followed by
/// <c>AddFeature</c> for each of <c>source.TrackedFeatureIds</c>.
/// </summary>
internal void MergeFrom(UserAgentDetails source)
{
if (source._userAgentCustomComponents.Count > 0)
{
AddUserAgentComponent(source.GetCustomUserAgentComponents());
}
foreach (var featureId in source._trackedFeatureIds)
{
_trackedFeatureIds.Add(featureId);
}
return string.Join(" ", _userAgentCustomComponents);
}

/// <summary>
Expand All @@ -90,7 +145,7 @@ private string GenerateMetricsUserAgent()
{
return string.Empty;
}
var metricsString = $"m/{string.Join(",", _trackedFeatureIds)}";
var metricsString = "m/" + string.Join(",", _trackedFeatureIds);
metricsString = TruncateToSize(metricsString);

return metricsString;
Expand All @@ -103,12 +158,12 @@ private string GenerateMetricsUserAgent()
/// <returns>A truncated version of the string within the byte limit.</returns>
private static string TruncateToSize(string input)
{
byte[] bytes = Encoding.UTF8.GetBytes(input);
if (bytes.Length <= MaxSizeBytes)
if (Encoding.UTF8.GetByteCount(input) <= MaxSizeBytes)
{
return input;
}

byte[] bytes = Encoding.UTF8.GetBytes(input);
var cutOffIndex = MaxSizeBytes;
while (cutOffIndex > 0 && bytes[cutOffIndex - 1] != ',')
{
Expand All @@ -118,4 +173,21 @@ private static string TruncateToSize(string input)
return Encoding.UTF8.GetString(bytes, 0, cutOffIndex - 1); // Remove last comma
}
}

/// <summary>
/// A generated User-Agent string together with the ordered components and feature ids it was generated from.
/// </summary>
internal sealed class GeneratedUserAgent
{
internal GeneratedUserAgent(string[] components, string[] featureIds, string value)
{
Components = components;
FeatureIds = featureIds;
Value = value;
}

internal string[] Components { get; }
internal string[] FeatureIds { get; }
internal string Value { get; }
}
}
Loading
Loading