From 0063e5ff1b206b9b0f092ba1c3846716337736d7 Mon Sep 17 00:00:00 2001 From: kasperk81 <83082615+kasperk81@users.noreply.github.com> Date: Fri, 24 May 2024 22:02:15 +0300 Subject: [PATCH] Remove all LINQ usage from product code --- CHANGELOG.md | 8 ++ src/HttpClientRequestAdapter.cs | 113 ++++++++++++------ src/KiotaClientFactory.cs | 84 ++++++++----- ...rosoft.Kiota.Http.HttpClientLibrary.csproj | 4 +- src/Middleware/ChaosHandler.cs | 3 +- src/Middleware/HeadersInspectionHandler.cs | 22 +++- .../Options/UserAgentHandlerOption.cs | 3 - .../ParametersNameDecodingHandler.cs | 74 ++++++++---- src/Middleware/RetryHandler.cs | 13 +- src/Middleware/UserAgentHandler.cs | 29 +++-- 10 files changed, 240 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c552716..55df23d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [Unreleased] + +## [1.4.3] - 2024-05-24 + +### Changed + +- Remove all LINQ usage from product code + ## [1.4.2] - 2024-05-21 ### Added diff --git a/src/HttpClientRequestAdapter.cs b/src/HttpClientRequestAdapter.cs index 51bc17c..1300ae7 100644 --- a/src/HttpClientRequestAdapter.cs +++ b/src/HttpClientRequestAdapter.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.Kiota.Abstractions; @@ -388,7 +387,7 @@ private async Task ThrowIfFailedResponse(HttpResponseMessage response, Dictionar var statusCodeAsInt = (int)response.StatusCode; var statusCodeAsString = statusCodeAsInt.ToString(); - var responseHeadersDictionary = response.Headers.ToDictionary(x => x.Key, y => y.Value, StringComparer.OrdinalIgnoreCase); + var responseHeadersDictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); ParsableFactory? errorFactory; if(errorMapping == null || !errorMapping.TryGetValue(statusCodeAsString, out errorFactory) && @@ -470,51 +469,85 @@ private async Task GetHttpResponseMessage(RequestInformatio var ex = new InvalidOperationException("Could not get a response after calling the service"); throw ex; } - if(response.Headers.TryGetValues("Content-Length", out var contentLengthValues) && - contentLengthValues.Any() && - contentLengthValues.First() is string firstContentLengthValue && - int.TryParse(firstContentLengthValue, out var contentLength)) + if(response.Headers.TryGetValues("Content-Length", out var contentLengthValues)) { - activityForAttributes?.SetTag("http.response_content_length", contentLength); + using var contentLengthEnumerator = contentLengthValues.GetEnumerator(); + if(contentLengthEnumerator.MoveNext() && int.TryParse(contentLengthEnumerator.Current, out var contentLength)) + { + activityForAttributes?.SetTag("http.response_content_length", contentLength); + } } - if(response.Headers.TryGetValues("Content-Type", out var contentTypeValues) && - contentTypeValues.Any() && - contentTypeValues.First() is string firstContentTypeValue) + if(response.Headers.TryGetValues("Content-Type", out var contentTypeValues)) { - activityForAttributes?.SetTag("http.response_content_type", firstContentTypeValue); + using var contentTypeEnumerator = contentTypeValues.GetEnumerator(); + if(contentTypeEnumerator.MoveNext()) + { + activityForAttributes?.SetTag("http.response_content_type", contentTypeEnumerator.Current); + } } activityForAttributes?.SetTag("http.status_code", (int)response.StatusCode); activityForAttributes?.SetTag("http.flavor", $"{response.Version.Major}.{response.Version.Minor}"); return await RetryCAEResponseIfRequired(response, requestInfo, cancellationToken, claims, activityForAttributes).ConfigureAwait(false); } + private static readonly Regex caeValueRegex = new("\"([^\"]*)\"", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); + /// /// The key for the event raised by tracing when an authentication challenge is received /// public const string AuthenticateChallengedEventKey = "com.microsoft.kiota.authenticate_challenge_received"; + private async Task RetryCAEResponseIfRequired(HttpResponseMessage response, RequestInformation requestInfo, CancellationToken cancellationToken, string? claims, Activity? activityForAttributes) { using var span = activitySource?.StartActivity(nameof(RetryCAEResponseIfRequired)); if(response.StatusCode == HttpStatusCode.Unauthorized && string.IsNullOrEmpty(claims) && // avoid infinite loop, we only retry once - (requestInfo.Content?.CanSeek ?? true) && - response.Headers.WwwAuthenticate?.FirstOrDefault(filterAuthHeader) is AuthenticationHeaderValue authHeader && - authHeader.Parameter?.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(static x => x.Trim()) - .FirstOrDefault(static x => x.StartsWith(ClaimsKey, StringComparison.OrdinalIgnoreCase)) is string rawResponseClaims && - caeValueRegex.Match(rawResponseClaims) is Match claimsMatch && - claimsMatch.Groups.Count > 1 && - claimsMatch.Groups[1].Value is string responseClaims) + (requestInfo.Content?.CanSeek ?? true)) { - span?.AddEvent(new ActivityEvent(AuthenticateChallengedEventKey)); - activityForAttributes?.SetTag("http.retry_count", 1); - requestInfo.Content?.Seek(0, SeekOrigin.Begin); - await DrainAsync(response, cancellationToken).ConfigureAwait(false); - return await GetHttpResponseMessage(requestInfo, cancellationToken, activityForAttributes, responseClaims).ConfigureAwait(false); + AuthenticationHeaderValue? authHeader = null; + foreach(var header in response.Headers.WwwAuthenticate) + { + if(filterAuthHeader(header)) + { + authHeader = header; + break; + } + } + + if(authHeader is not null) + { + var authHeaderParameters = authHeader.Parameter?.Split([','], StringSplitOptions.RemoveEmptyEntries); + string? rawResponseClaims = null; + if(authHeaderParameters != null) + { + foreach(var parameter in authHeaderParameters) + { + var trimmedParameter = parameter.Trim(); + if(trimmedParameter.StartsWith(ClaimsKey, StringComparison.OrdinalIgnoreCase)) + { + rawResponseClaims = trimmedParameter; + break; + } + } + } + + if(rawResponseClaims != null && + caeValueRegex.Match(rawResponseClaims) is Match claimsMatch && + claimsMatch.Groups.Count > 1 && + claimsMatch.Groups[1].Value is string responseClaims) + { + span?.AddEvent(new ActivityEvent(AuthenticateChallengedEventKey)); + activityForAttributes?.SetTag("http.retry_count", 1); + requestInfo.Content?.Seek(0, SeekOrigin.Begin); + await DrainAsync(response, cancellationToken).ConfigureAwait(false); + return await GetHttpResponseMessage(requestInfo, cancellationToken, activityForAttributes, responseClaims).ConfigureAwait(false); + } + } } return response; } + private void SetBaseUrlForRequestInformation(RequestInformation requestInfo) { IDictionaryExtensions.AddOrReplace(requestInfo.PathParameters, "baseurl", BaseUrl!); @@ -527,6 +560,7 @@ private void SetBaseUrlForRequestInformation(RequestInformation requestInfo) return result; else throw new InvalidOperationException($"Could not convert the request information to a {typeof(T).Name}"); } + private HttpRequestMessage GetRequestMessageFromRequestInformation(RequestInformation requestInfo, Activity? activityForAttributes) { using var span = activitySource?.StartActivity(nameof(GetRequestMessageFromRequestInformation)); @@ -544,37 +578,42 @@ private HttpRequestMessage GetRequestMessageFromRequestInformation(RequestInform Version = new Version(2, 0) }; - if(requestInfo.RequestOptions.Any()) + if(requestInfo.RequestOptions != null) #if NET5_0_OR_GREATER { - requestInfo.RequestOptions.ToList().ForEach(x => message.Options.Set(new HttpRequestOptionsKey(x.GetType().FullName!), x)); + foreach (var option in requestInfo.RequestOptions) + message.Options.Set(new HttpRequestOptionsKey(option.GetType().FullName!), option); } message.Options.Set(new HttpRequestOptionsKey(typeof(ObservabilityOptions).FullName!), obsOptions); #else { - requestInfo.RequestOptions.ToList().ForEach(x => IDictionaryExtensions.TryAdd(message.Properties, x.GetType().FullName!, x)); + foreach(var option in requestInfo.RequestOptions) + IDictionaryExtensions.TryAdd(message.Properties, option.GetType().FullName!, option); } IDictionaryExtensions.TryAdd(message.Properties!, typeof(ObservabilityOptions).FullName, obsOptions); #endif if(requestInfo.Content != null && requestInfo.Content != Stream.Null) message.Content = new StreamContent(requestInfo.Content); - if(requestInfo.Headers?.Any() ?? false) + if(requestInfo.Headers != null) foreach(var header in requestInfo.Headers) if(!message.Headers.TryAddWithoutValidation(header.Key, header.Value) && message.Content != null) message.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);// Try to add the headers we couldn't add to the HttpRequestMessage before to the HttpContent if(message.Content != null) { - if(message.Content.Headers.TryGetValues("Content-Length", out var contentLenValues) && - contentLenValues.Any() && - contentLenValues.First() is string contentLenValue && - int.TryParse(contentLenValue, out var contentLenValueInt)) - activityForAttributes?.SetTag("http.request_content_length", contentLenValueInt); - if(message.Content.Headers.TryGetValues("Content-Type", out var contentTypeValues) && - contentTypeValues.Any() && - contentTypeValues.First() is string contentTypeValue) - activityForAttributes?.SetTag("http.request_content_type", contentTypeValue); + if(message.Content.Headers.TryGetValues("Content-Length", out var contentLenValues)) + { + var contentLenEnumerator = contentLenValues.GetEnumerator(); + if(contentLenEnumerator.MoveNext() && int.TryParse(contentLenEnumerator.Current, out var contentLenValueInt)) + activityForAttributes?.SetTag("http.request_content_length", contentLenValueInt); + } + if(message.Content.Headers.TryGetValues("Content-Type", out var contentTypeValues)) + { + var contentTypeEnumerator = contentTypeValues.GetEnumerator(); + if(contentTypeEnumerator.MoveNext()) + activityForAttributes?.SetTag("http.request_content_type", contentTypeEnumerator.Current); + } } return message; } diff --git a/src/KiotaClientFactory.cs b/src/KiotaClientFactory.cs index 4e4ca05..c9d99da 100644 --- a/src/KiotaClientFactory.cs +++ b/src/KiotaClientFactory.cs @@ -2,7 +2,7 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. // ------------------------------------------------------------------------------ -using System.Linq; +using System; using System.Collections.Generic; using System.Net; using System.Net.Http; @@ -26,8 +26,17 @@ public static class KiotaClientFactory /// The with the default middlewares. public static HttpClient Create(HttpMessageHandler? finalHandler = null, IRequestOption[]? optionsForHandlers = null) { - var defaultHandlers = CreateDefaultHandlers(optionsForHandlers); - var handler = ChainHandlersCollectionAndGetFirstLink(finalHandler ?? GetDefaultHttpMessageHandler(), defaultHandlers.ToArray()); + var defaultHandlersEnumerable = CreateDefaultHandlers(optionsForHandlers); + int count = 0; + foreach(var _ in defaultHandlersEnumerable) count++; + + var defaultHandlersArray = new DelegatingHandler[count]; + int index = 0; + foreach(var handler2 in defaultHandlersEnumerable) + { + defaultHandlersArray[index++] = handler2; + } + var handler = ChainHandlersCollectionAndGetFirstLink(finalHandler ?? GetDefaultHttpMessageHandler(), defaultHandlersArray); return handler != null ? new HttpClient(handler) : new HttpClient(); } @@ -39,9 +48,16 @@ public static HttpClient Create(HttpMessageHandler? finalHandler = null, IReques /// The with the custom handlers. public static HttpClient Create(IList handlers, HttpMessageHandler? finalHandler = null) { - if(handlers == null || !handlers.Any()) + if(handlers == null || handlers.Count == 0) return Create(finalHandler); - var handler = ChainHandlersCollectionAndGetFirstLink(finalHandler ?? GetDefaultHttpMessageHandler(), handlers.ToArray()); + + DelegatingHandler[] handlersArray = new DelegatingHandler[handlers.Count]; + for(int i = 0; i < handlers.Count; i++) + { + handlersArray[i] = handlers[i]; + } + + var handler = ChainHandlersCollectionAndGetFirstLink(finalHandler ?? GetDefaultHttpMessageHandler(), handlersArray); return handler != null ? new HttpClient(handler) : new HttpClient(); } @@ -51,35 +67,39 @@ public static HttpClient Create(IList handlers, HttpMessageHa /// A list of the default handlers used by the client. public static IList CreateDefaultHandlers(IRequestOption[]? optionsForHandlers = null) { - optionsForHandlers ??= []; - - return new List - { - //add the default middlewares as they are ready, and add them to the list below as well - - optionsForHandlers.OfType().FirstOrDefault() is UriReplacementHandlerOption uriReplacementOption - ? new UriReplacementHandler(uriReplacementOption) - : new UriReplacementHandler(), - - optionsForHandlers.OfType().FirstOrDefault() is RetryHandlerOption retryHandlerOption - ? new RetryHandler(retryHandlerOption) - : new RetryHandler(), - - optionsForHandlers.OfType().FirstOrDefault() is RedirectHandlerOption redirectHandlerOption - ? new RedirectHandler(redirectHandlerOption) - : new RedirectHandler(), + optionsForHandlers ??= Array.Empty(); - optionsForHandlers.OfType().FirstOrDefault() is ParametersNameDecodingOption parametersNameDecodingOption - ? new ParametersNameDecodingHandler(parametersNameDecodingOption) - : new ParametersNameDecodingHandler(), + UriReplacementHandlerOption? uriReplacementOption = null; + RetryHandlerOption? retryHandlerOption = null; + RedirectHandlerOption? redirectHandlerOption = null; + ParametersNameDecodingOption? parametersNameDecodingOption = null; + UserAgentHandlerOption? userAgentHandlerOption = null; + HeadersInspectionHandlerOption? headersInspectionHandlerOption = null; - optionsForHandlers.OfType().FirstOrDefault() is UserAgentHandlerOption userAgentHandlerOption - ? new UserAgentHandler(userAgentHandlerOption) - : new UserAgentHandler(), + foreach(var option in optionsForHandlers) + { + if(uriReplacementOption == null && option is UriReplacementHandlerOption uriOption) + uriReplacementOption = uriOption; + else if(retryHandlerOption == null && option is RetryHandlerOption retryOption) + retryHandlerOption = retryOption; + else if(redirectHandlerOption == null && option is RedirectHandlerOption redirectOption) + redirectHandlerOption = redirectOption; + else if(parametersNameDecodingOption == null && option is ParametersNameDecodingOption parametersOption) + parametersNameDecodingOption = parametersOption; + else if(userAgentHandlerOption == null && option is UserAgentHandlerOption userAgentOption) + userAgentHandlerOption = userAgentOption; + else if(headersInspectionHandlerOption == null && option is HeadersInspectionHandlerOption headersOption) + headersInspectionHandlerOption = headersOption; + } - optionsForHandlers.OfType().FirstOrDefault() is HeadersInspectionHandlerOption headersInspectionHandlerOption - ? new HeadersInspectionHandler(headersInspectionHandlerOption) - : new HeadersInspectionHandler(), + return new List + { + uriReplacementOption != null ? new UriReplacementHandler(uriReplacementOption) : new UriReplacementHandler(), + retryHandlerOption != null ? new RetryHandler(retryHandlerOption) : new RetryHandler(), + redirectHandlerOption != null ? new RedirectHandler(redirectHandlerOption) : new RedirectHandler(), + parametersNameDecodingOption != null ? new ParametersNameDecodingHandler(parametersNameDecodingOption) : new ParametersNameDecodingHandler(), + userAgentHandlerOption != null ? new UserAgentHandler(userAgentHandlerOption) : new UserAgentHandler(), + headersInspectionHandlerOption != null ? new HeadersInspectionHandler(headersInspectionHandlerOption) : new HeadersInspectionHandler(), }; } @@ -109,7 +129,7 @@ public static IList CreateDefaultHandlers(IRequestOption[]? o /// The created . public static DelegatingHandler? ChainHandlersCollectionAndGetFirstLink(HttpMessageHandler? finalHandler, params DelegatingHandler[] handlers) { - if(handlers == null || !handlers.Any()) return default; + if(handlers == null || handlers.Length == 0) return default; var handlersCount = handlers.Length; for(var i = 0; i < handlersCount; i++) { diff --git a/src/Microsoft.Kiota.Http.HttpClientLibrary.csproj b/src/Microsoft.Kiota.Http.HttpClientLibrary.csproj index f03b2c8..d44b149 100644 --- a/src/Microsoft.Kiota.Http.HttpClientLibrary.csproj +++ b/src/Microsoft.Kiota.Http.HttpClientLibrary.csproj @@ -15,7 +15,7 @@ https://aka.ms/kiota/docs true true - 1.4.2 + 1.4.3 true @@ -73,4 +73,4 @@ - \ No newline at end of file + diff --git a/src/Middleware/ChaosHandler.cs b/src/Middleware/ChaosHandler.cs index 89cd67d..57b62a3 100644 --- a/src/Middleware/ChaosHandler.cs +++ b/src/Middleware/ChaosHandler.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -103,7 +102,7 @@ private HttpResponseMessage CreateChaosResponse(List knownF private void LoadKnownFailures(List? knownFailures) { - if(knownFailures?.Any() ?? false) + if(knownFailures?.Count > 0) { _knownFailures = knownFailures; } diff --git a/src/Middleware/HeadersInspectionHandler.cs b/src/Middleware/HeadersInspectionHandler.cs index 96e4f14..560b30e 100644 --- a/src/Middleware/HeadersInspectionHandler.cs +++ b/src/Middleware/HeadersInspectionHandler.cs @@ -3,8 +3,8 @@ // ------------------------------------------------------------------------------ using System; +using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -19,6 +19,7 @@ namespace Microsoft.Kiota.Http.HttpClientLibrary.Middleware; public class HeadersInspectionHandler : DelegatingHandler { private readonly HeadersInspectionHandlerOption _defaultOptions; + /// /// Create a new instance of /// @@ -27,6 +28,7 @@ public HeadersInspectionHandler(HeadersInspectionHandlerOption? defaultOptions = { _defaultOptions = defaultOptions ?? new HeadersInspectionHandlerOption(); } + /// protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { @@ -51,12 +53,12 @@ protected override async Task SendAsync(HttpRequestMessage { foreach(var header in request.Headers) { - options.RequestHeaders[header.Key] = header.Value.ToArray(); + options.RequestHeaders[header.Key] = ConvertHeaderValuesToArray(header.Value); } if(request.Content != null) foreach(var contentHeaders in request.Content.Headers) { - options.RequestHeaders[contentHeaders.Key] = contentHeaders.Value.ToArray(); + options.RequestHeaders[contentHeaders.Key] = ConvertHeaderValuesToArray(contentHeaders.Value); } } var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); @@ -64,12 +66,12 @@ protected override async Task SendAsync(HttpRequestMessage { foreach(var header in response.Headers) { - options.ResponseHeaders[header.Key] = header.Value.ToArray(); + options.ResponseHeaders[header.Key] = ConvertHeaderValuesToArray(header.Value); } if(response.Content != null) foreach(var contentHeaders in response.Content.Headers) { - options.ResponseHeaders[contentHeaders.Key] = contentHeaders.Value.ToArray(); + options.ResponseHeaders[contentHeaders.Key] = ConvertHeaderValuesToArray(contentHeaders.Value); } } return response; @@ -79,6 +81,14 @@ protected override async Task SendAsync(HttpRequestMessage activity?.Dispose(); } + string[] ConvertHeaderValuesToArray(IEnumerable headerValues) + { + var headerValuesList = new List(); + foreach(var value in headerValues) + { + headerValuesList.Add(value); + } + return headerValuesList.ToArray(); + } } - } diff --git a/src/Middleware/Options/UserAgentHandlerOption.cs b/src/Middleware/Options/UserAgentHandlerOption.cs index 17a0e91..de75896 100644 --- a/src/Middleware/Options/UserAgentHandlerOption.cs +++ b/src/Middleware/Options/UserAgentHandlerOption.cs @@ -2,9 +2,6 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. // ------------------------------------------------------------------------------ -using System.Diagnostics; -using System.Linq; -using System.Reflection; using Microsoft.Kiota.Abstractions; namespace Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options diff --git a/src/Middleware/ParametersNameDecodingHandler.cs b/src/Middleware/ParametersNameDecodingHandler.cs index 513deb9..15973bb 100644 --- a/src/Middleware/ParametersNameDecodingHandler.cs +++ b/src/Middleware/ParametersNameDecodingHandler.cs @@ -3,8 +3,8 @@ // ------------------------------------------------------------------------------ using System; +using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -16,12 +16,15 @@ namespace Microsoft.Kiota.Http.HttpClientLibrary.Middleware; /// /// This handlers decodes special characters in the request query parameters that had to be encoded due to RFC 6570 restrictions names before executing the request. /// -public class ParametersNameDecodingHandler: DelegatingHandler +public class ParametersNameDecodingHandler : DelegatingHandler { /// /// The options to use when decoding parameters names in URLs /// - internal ParametersNameDecodingOption EncodingOptions { get; set; } + internal ParametersNameDecodingOption EncodingOptions + { + get; set; + } /// /// Constructs a new /// @@ -30,22 +33,28 @@ public ParametersNameDecodingHandler(ParametersNameDecodingOption? options = def { EncodingOptions = options ?? new(); } + + /// protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var options = request.GetRequestOption() ?? EncodingOptions; Activity? activity; - if (request.GetRequestOption() is { } obsOptions) { + if(request.GetRequestOption() is { } obsOptions) + { var activitySource = ActivitySourceRegistry.DefaultInstance.GetOrCreateActivitySource(obsOptions.TracerInstrumentationName); activity = activitySource.StartActivity($"{nameof(ParametersNameDecodingHandler)}_{nameof(SendAsync)}"); activity?.SetTag("com.microsoft.kiota.handler.parameters_name_decoding.enable", true); - } else { + } + else + { activity = null; } - try { - if(!request.RequestUri!.Query.Contains('%') || + try + { + if(!request.RequestUri!.Query.Contains("%") || !options.Enabled || - !(options.ParametersToDecode?.Any() ?? false)) + options.ParametersToDecode == null || options.ParametersToDecode.Count == 0) { return base.SendAsync(request, cancellationToken); } @@ -55,26 +64,51 @@ protected override Task SendAsync(HttpRequestMessage reques var decodedUri = new UriBuilder(originalUri.Scheme, originalUri.Host, originalUri.Port, originalUri.AbsolutePath, query).Uri; request.RequestUri = decodedUri; return base.SendAsync(request, cancellationToken); - } finally { + } + finally + { activity?.Dispose(); } } - internal static string? DecodeUriEncodedString(string? original, char[] charactersToDecode) { - if (string.IsNullOrEmpty(original) || !(charactersToDecode?.Any() ?? false)) + + internal static string? DecodeUriEncodedString(string? original, char[] charactersToDecode) + { + if(string.IsNullOrEmpty(original) || charactersToDecode == null || charactersToDecode.Length == 0) return original; - var symbolsToReplace = charactersToDecode.Select(static x => ($"%{Convert.ToInt32(x):X}", x.ToString())).Where(x => original!.Contains(x.Item1)).ToArray(); - var encodedParameterValues = original!.TrimStart('?') - .Split(new []{'&'}, StringSplitOptions.RemoveEmptyEntries) - .Select(static part => part.Split(new []{'='}, StringSplitOptions.RemoveEmptyEntries)[0]) - .Where(static x => x.Contains('%'))// only pull out params with `%` (encoded) - .ToArray(); + var symbolsToReplace = new List<(string, string)>(); + foreach(var character in charactersToDecode) + { + var symbol = ($"%{Convert.ToInt32(character):X}", character.ToString()); + if(original?.Contains(symbol.Item1) ?? false) + { + symbolsToReplace.Add(symbol); + } + } + + var encodedParameterValues = new List(); + var parts = original?.TrimStart('?').Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries); + if(parts is null) return original; + foreach(var part in parts) + { + var parameter = part.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries)[0]; + if(parameter.Contains("%")) // only pull out params with `%` (encoded) + { + encodedParameterValues.Add(parameter); + } + } foreach(var parameter in encodedParameterValues) { - var updatedParameterName = symbolsToReplace.Where(x => parameter!.Contains(x.Item1)) - .Aggregate(parameter, (current, symbolToReplace) => current!.Replace(symbolToReplace.Item1, symbolToReplace.Item2)); - original = original.Replace(parameter, updatedParameterName); + var updatedParameterName = parameter; + foreach(var symbolToReplace in symbolsToReplace) + { + if(parameter.Contains(symbolToReplace.Item1)) + { + updatedParameterName = updatedParameterName.Replace(symbolToReplace.Item1, symbolToReplace.Item2); + } + } + original = original?.Replace(parameter, updatedParameterName); } return original; diff --git a/src/Middleware/RetryHandler.cs b/src/Middleware/RetryHandler.cs index 48f8a0d..94aaa47 100644 --- a/src/Middleware/RetryHandler.cs +++ b/src/Middleware/RetryHandler.cs @@ -5,9 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using System.Net; using System.Net.Http; using System.Threading; @@ -183,7 +181,8 @@ internal static Task Delay(HttpResponseMessage response, int retryCount, int del delayInSeconds = delay; if(response.Headers.TryGetValues(RetryAfter, out IEnumerable? values)) { - string retryAfter = values.First(); + using IEnumerator v = values.GetEnumerator(); + string retryAfter = v.MoveNext() ? v.Current : throw new InvalidOperationException("Retry-After header is empty."); // the delay could be in the form of a seconds or a http date. See https://httpwg.org/specs/rfc7231.html#header.retry-after if(int.TryParse(retryAfter, out int delaySeconds)) { @@ -244,10 +243,16 @@ private static async Task GetInnerExceptionAsync(HttpResponseMessage errorMessage = await response.Content.ReadAsStringAsync().ConfigureAwait(false); } + var headersDictionary = new Dictionary>(); + foreach(var header in response.Headers) + { + headersDictionary.Add(header.Key, header.Value); + } + return new ApiException($"HTTP request failed with status code: {response.StatusCode}.{errorMessage}") { ResponseStatusCode = (int)response.StatusCode, - ResponseHeaders = response.Headers.ToDictionary(header => header.Key, header => header.Value), + ResponseHeaders = headersDictionary, }; } } diff --git a/src/Middleware/UserAgentHandler.cs b/src/Middleware/UserAgentHandler.cs index 8b7a867..e62276e 100644 --- a/src/Middleware/UserAgentHandler.cs +++ b/src/Middleware/UserAgentHandler.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; @@ -31,23 +30,39 @@ protected override Task SendAsync(HttpRequestMessage reques throw new ArgumentNullException(nameof(request)); Activity? activity; - if (request.GetRequestOption() is { } obsOptions) { + if(request.GetRequestOption() is { } obsOptions) + { var activitySource = ActivitySourceRegistry.DefaultInstance.GetOrCreateActivitySource(obsOptions.TracerInstrumentationName); activity = activitySource?.StartActivity($"{nameof(UserAgentHandler)}_{nameof(SendAsync)}"); activity?.SetTag("com.microsoft.kiota.handler.useragent.enable", true); - } else { + } + else + { activity = null; } - try { + + try + { var userAgentHandlerOption = request.GetRequestOption() ?? _userAgentOption; - if(userAgentHandlerOption.Enabled && - !request.Headers.UserAgent.Any(x => userAgentHandlerOption.ProductName.Equals(x.Product?.Name, StringComparison.OrdinalIgnoreCase))) + bool isProductNamePresent = false; + foreach(var userAgent in request.Headers.UserAgent) + { + if(userAgentHandlerOption.ProductName.Equals(userAgent.Product?.Name, StringComparison.OrdinalIgnoreCase)) + { + isProductNamePresent = true; + break; + } + } + + if(userAgentHandlerOption.Enabled && !isProductNamePresent) { request.Headers.UserAgent.Add(new ProductInfoHeaderValue(userAgentHandlerOption.ProductName, userAgentHandlerOption.ProductVersion)); } return base.SendAsync(request, cancellationToken); - } finally { + } + finally + { activity?.Dispose(); } }