| File: NugetSearch\NugetToolSearchApiRequest.cs | Web Access |
| Project: src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable disable using System.Collections.Specialized; using System.Web; #if CLI_AOT using System.Text.Json; using System.Text.Json.Serialization; #else using NuGet.Protocol; using NuGet.Protocol.Core.Types; #endif namespace Microsoft.DotNet.Cli.NugetSearch; internal class NugetToolSearchApiRequest : INugetToolSearchApiRequest { public async Task<string> GetResult(NugetSearchApiParameter nugetSearchApiParameter) { var queryUrl = await ConstructUrl( nugetSearchApiParameter.SearchTerm, nugetSearchApiParameter.Skip, nugetSearchApiParameter.Take, nugetSearchApiParameter.Prerelease); var httpClient = new HttpClient(); using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(10)); HttpResponseMessage response = await httpClient.GetAsync(queryUrl, cancellation.Token); if (!response.IsSuccessStatusCode) { if ((int)response.StatusCode >= 500 && (int)response.StatusCode < 600) { throw new NugetSearchApiRequestException( string.Format( CliStrings.RetriableNugetSearchFailure, queryUrl.AbsoluteUri, response.ReasonPhrase, response.StatusCode)); } throw new NugetSearchApiRequestException( string.Format( CliStrings.NonRetriableNugetSearchFailure, queryUrl.AbsoluteUri, response.ReasonPhrase, response.StatusCode)); } return await response.Content.ReadAsStringAsync(cancellation.Token); } internal static async Task<Uri> ConstructUrl(string searchTerm = null, int? skip = null, int? take = null, bool prerelease = false, Uri domainAndPathOverride = null) { var uriBuilder = new UriBuilder(domainAndPathOverride ?? await DomainAndPath()); NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query); if (!string.IsNullOrWhiteSpace(searchTerm)) { query["q"] = searchTerm; } query["packageType"] = "dotnettool"; // This is a field for internal nuget back // compatibility should be "2.0.0" for all new API usage query["semVerLevel"] = "2.0.0"; if (skip.HasValue) { query["skip"] = skip.Value.ToString(); } if (take.HasValue) { query["take"] = take.Value.ToString(); } if (prerelease) { query["prerelease"] = "true"; } uriBuilder.Query = query.ToString(); return uriBuilder.Uri; } // More detail on this API https://github.com/dotnet/sdk/issues/12038 #if CLI_AOT // NuGet.Protocol's service-index resolution isn't AOT-compatible, so under AOT we read the // service index directly over HTTP and select the SearchQueryService endpoint using // System.Text.Json source generation. Failures are translated into NugetSearchApiRequestException // (a GracefulException) with the same retriable/non-retriable messaging as GetResult. private static async Task<Uri> DomainAndPath() { const string serviceIndexUrl = "https://api.nuget.org/v3/index.json"; const string searchQueryServiceType = "SearchQueryService/3.5.0"; using var httpClient = new HttpClient(); using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(10)); HttpResponseMessage response; try { response = await httpClient.GetAsync(serviceIndexUrl, cancellation.Token); } catch (Exception e) when (e is HttpRequestException or OperationCanceledException) { // Transient network failures (DNS, connection refused, timeout) are retriable. throw new NugetSearchApiRequestException( string.Format( CliStrings.RetriableNugetSearchFailure, serviceIndexUrl, e.Message, "N/A")); } using (response) { if (!response.IsSuccessStatusCode) { if ((int)response.StatusCode >= 500 && (int)response.StatusCode < 600) { throw new NugetSearchApiRequestException( string.Format( CliStrings.RetriableNugetSearchFailure, serviceIndexUrl, response.ReasonPhrase, response.StatusCode)); } throw new NugetSearchApiRequestException( string.Format( CliStrings.NonRetriableNugetSearchFailure, serviceIndexUrl, response.ReasonPhrase, response.StatusCode)); } var indexJson = await response.Content.ReadAsStringAsync(cancellation.Token); NugetServiceIndex index; try { index = JsonSerializer.Deserialize(indexJson, NugetServiceIndexJsonSerializerContext.Default.NugetServiceIndex); } catch (JsonException e) { throw new NugetSearchApiRequestException( string.Format( CliStrings.NonRetriableNugetSearchFailure, serviceIndexUrl, e.Message, response.StatusCode)); } var resource = index?.Resources?.FirstOrDefault(r => r.Type == searchQueryServiceType) ?? throw new NugetSearchApiRequestException( string.Format( CliStrings.NonRetriableNugetSearchFailure, serviceIndexUrl, $"{searchQueryServiceType} not found in service index", response.StatusCode)); // The service index is server-supplied, so don't trust the URL to be well-formed. try { return new Uri(resource.Id); } catch (Exception e) when (e is UriFormatException or ArgumentException) { throw new NugetSearchApiRequestException( string.Format( CliStrings.NonRetriableNugetSearchFailure, serviceIndexUrl, $"{searchQueryServiceType} returned a malformed URL: {e.Message}", response.StatusCode)); } } } #else private static async Task<Uri> DomainAndPath() { var repository = Repository.Factory.GetCoreV3("https://api.nuget.org/v3/index.json"); var resource = await repository.GetResourceAsync<ServiceIndexResourceV3>(); var uris = resource.GetServiceEntryUris("SearchQueryService/3.5.0"); return uris[0]; } #endif } #if CLI_AOT internal sealed class NugetServiceIndex { [JsonPropertyName("resources")] public NugetServiceResource[] Resources { get; set; } } internal sealed class NugetServiceResource { [JsonPropertyName("@id")] public string Id { get; set; } [JsonPropertyName("@type")] public string Type { get; set; } } [JsonSerializable(typeof(NugetServiceIndex))] internal partial class NugetServiceIndexJsonSerializerContext : JsonSerializerContext; #endif