using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; using Amazon.SellingPartnerApiSDK.AmazonSpApiSDK.Models.Exceptions; using Amazon.SellingPartnerApiSDK.AmazonSpApiSDK.Models.Token; using Amazon.SellingPartnerApiSDK.Misc; using Amazon.SellingPartnerApiSDK.Parameters; using Newtonsoft.Json; using RestSharp; using RestSharp.Serializers.NewtonsoftJson; using Method = RestSharp.Method; using static Amazon.SellingPartnerApiSDK.Misc.Constants; using static Amazon.SellingPartnerApiSDK.AmazonSpApiSDK.Models.Token.CacheTokenData; namespace Amazon.SellingPartnerApiSDK.Services { public class RequestService : ApiUrls { public static readonly string AccessTokenHeaderName = "x-amz-access-token"; public static readonly string SecurityTokenHeaderName = "x-amz-security-token"; public static readonly string ShippingBusinessIdHeaderName = "x-amzn-shipping-business-id"; private const string RateLimitLimitHeaderName = "x-amzn-RateLimit-Limit"; protected RequestService(AmazonCredential amazonCredential) { AmazonCredential = amazonCredential; AmazonSandboxUrl = amazonCredential.MarketPlace.Region.SandboxHostUrl; AmazonProductionUrl = amazonCredential.MarketPlace.Region.HostUrl; } protected RestClient RequestClient { get; set; } protected RestRequest Request { get; set; } protected AmazonCredential AmazonCredential { get; set; } protected string AmazonSandboxUrl { get; set; } protected string AmazonProductionUrl { get; set; } protected string AccessToken { get; set; } protected IList> LastHeaders { get; set; } protected string ApiBaseUrl => AmazonCredential.Environment == Constants.Environments.Sandbox ? AmazonSandboxUrl : AmazonProductionUrl; public IList> LastResponseHeader => LastHeaders; private void CreateRequest(string url, Method method) { if (string.IsNullOrWhiteSpace(AmazonCredential.ProxyAddress)) { var options = new RestClientOptions(ApiBaseUrl); RequestClient = new RestClient(options, configureSerialization: s => s.UseNewtonsoftJson()); } else { var options = new RestClientOptions(ApiBaseUrl) { Proxy = new WebProxy() { Address = new Uri(AmazonCredential.ProxyAddress) } }; RequestClient = new RestClient(options, configureSerialization: s => s.UseNewtonsoftJson()); } Request = new RestRequest(url, method); } protected async Task CreateAuthorizedRequestAsync(string url, Method method, List> queryParameters = null, object postJsonObj = null, CacheTokenData.TokenDataType tokenDataType = CacheTokenData.TokenDataType.Normal, object parameter = null, CancellationToken cancellationToken = default) { if (parameter is IParameterBasedPII piiObject && piiObject.IsNeedRestrictedDataToken) { await RefreshTokenAsync(CacheTokenData.TokenDataType.PII, piiObject.RestrictedDataTokenRequest, cancellationToken); } else { await RefreshTokenAsync(tokenDataType, cancellationToken: cancellationToken); } CreateRequest(url, method); if (postJsonObj != null) AddJsonBody(postJsonObj); if (queryParameters != null) AddQueryParameters(queryParameters); } private async Task ExecuteRequestTry(RateLimitType rateLimitType = RateLimitType.UNSET, CancellationToken cancellationToken = default) where T : new() { RestHeader(); AddAccessToken(); var response = await RequestClient.ExecuteAsync(Request, cancellationToken); LogRequest(Request, response); SaveLastRequestHeader(response.Headers); await SleepForRateLimit(response.Headers, rateLimitType, cancellationToken); ParseResponse(response); if (response.StatusCode == HttpStatusCode.OK && !string.IsNullOrEmpty(response.Content) && response.Data == null) response.Data = JsonConvert.DeserializeObject(response.Content); return response.Data; } private void SaveLastRequestHeader(IReadOnlyCollection parameters) { LastHeaders = new List>(); foreach (RestSharp.Parameter parameter in parameters ?? Enumerable.Empty()) if (parameter != null && parameter.Name != null && parameter.Value != null) LastHeaders.Add(new KeyValuePair(parameter.Name, parameter.Value.ToString())); } private void LogRequest(RestRequest request, RestResponse response) { if (AmazonCredential.IsDebugMode) { var requestToLog = new { resource = request.Resource, parameters = request.Parameters.Select(parameter => new { name = parameter.Name, value = parameter.Value, type = parameter.Type.ToString() }), method = request.Method.ToString() }; var responseToLog = new { statusCode = response.StatusCode, content = response.Content, headers = response.Headers, responseUri = response.ResponseUri, errorMessage = response.ErrorMessage }; Console.WriteLine("\n\n"); Console.WriteLine("Request completed, \nRequest: {0} \n\nResponse: {1}", JsonConvert.SerializeObject(requestToLog), JsonConvert.SerializeObject(responseToLog)); } } private void RestHeader() { lock (Request) { Request?.Parameters?.RemoveParameter(AccessTokenHeaderName); Request?.Parameters?.RemoveParameter(SecurityTokenHeaderName); Request?.Parameters?.RemoveParameter(ShippingBusinessIdHeaderName); } } protected async Task ExecuteRequestAsync(RateLimitType rateLimitType = RateLimitType.UNSET, CancellationToken cancellationToken = default) where T : new() { var tryCount = 0; while (true) try { return await ExecuteRequestTry(rateLimitType, cancellationToken); } catch (AmazonQuotaExceededException ex) { if (tryCount >= AmazonCredential.MaxThrottledRetryCount) { if (AmazonCredential.IsDebugMode) Console.WriteLine("已达到最大的尝试次数"); throw; } cancellationToken.ThrowIfCancellationRequested(); AmazonCredential.UsagePlansTimings[rateLimitType].Delay(); tryCount++; } } private async Task SleepForRateLimit(IReadOnlyCollection headers, RateLimitType rateLimitType = RateLimitType.UNSET, CancellationToken cancellationToken = default) { try { decimal rate = 0; var limitHeader = headers.FirstOrDefault(a => a.Name == RateLimitLimitHeaderName); if (limitHeader != null) { var rateLimitValue = limitHeader.Value.ToString(); decimal.TryParse(rateLimitValue, NumberStyles.Any, CultureInfo.InvariantCulture, out rate); } if (AmazonCredential.IsActiveLimitRate) { if (rateLimitType == RateLimitType.UNSET) { if (rate > 0) { var sleepTime = (int)(1 / rate * 1000); await Task.Delay(sleepTime, cancellationToken); } } else { if (rate > 0) AmazonCredential.UsagePlansTimings[rateLimitType].SetRateLimit(rate); var time = AmazonCredential.UsagePlansTimings[rateLimitType].NextRate(rateLimitType); } } } catch (Exception) { // ignored } } private void ParseResponse(RestResponse response) { if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Accepted || response.StatusCode == HttpStatusCode.Created || response.StatusCode == HttpStatusCode.NoContent) return; else if (response.StatusCode == HttpStatusCode.NotFound) { throw new AmazonNotFoundException("Resource that you are looking for is not found", response); } else { if (AmazonCredential.IsDebugMode) Console.WriteLine("Amazon Api didn't respond with Okay, see exception for more details" + response.Content); var errorResponse = response.Content.ConvertToErrorResponse(); if (errorResponse != null) { var error = errorResponse.Errors.FirstOrDefault(); switch (error.Code) { case "Unauthorized": throw new AmazonUnauthorizedException(error.Message, response); case "InvalidSignature": throw new AmazonInvalidSignatureException(error.Message, response); case "InvalidInput": throw new AmazonInvalidInputException(error.Message, error.Details, response); case "QuotaExceeded": throw new AmazonQuotaExceededException(error.Message, response); case "InternalFailure": throw new AmazonInternalErrorException(error.Message, response); } } } if (response.StatusCode == HttpStatusCode.BadRequest) { throw new AmazonBadRequestException( "BadRequest see https://developer-docs.amazon.com/sp-api/changelog/api-request-validation-for-400-errors-with-html-response for advice", response); } throw new AmazonException("Amazon Api didn't respond with Okay, see exception for more details", response); } private void AddQueryParameters(List> queryParameters) { if (queryParameters != null) queryParameters.ForEach(qp => Request.AddQueryParameter(qp.Key, qp.Value)); } private void AddJsonBody(object jsonData) { var json = JsonConvert.SerializeObject(jsonData); Request.AddJsonBody(json); } private void AddAccessToken() { lock (Request) { Request.AddOrUpdateHeader(AccessTokenHeaderName, AccessToken); } } private async Task RefreshTokenAsync(CacheTokenData.TokenDataType tokenDataType = CacheTokenData.TokenDataType.Normal, CreateRestrictedDataTokenRequest requestPii = null, CancellationToken cancellationToken = default) { var token = AmazonCredential.GetToken(tokenDataType); if (token == null) { if (tokenDataType == CacheTokenData.TokenDataType.PII) { var pii = await CreateRestrictedDataTokenAsync(requestPii, cancellationToken); if (pii != null) token = new TokenResponse { access_token = pii.RestrictedDataToken, expires_in = pii.ExpiresIn }; else throw new ArgumentNullException(nameof(pii)); } else { token = await TokenGeneration.RefreshAccessTokenAsync(AmazonCredential, tokenDataType, cancellationToken); } AmazonCredential.SetToken(tokenDataType, token); } AccessToken = token.access_token; } private async Task CreateRestrictedDataTokenAsync( CreateRestrictedDataTokenRequest createRestrictedDataTokenRequest, CancellationToken cancellationToken = default) { await CreateAuthorizedRequestAsync(TokenApiUrls.RestrictedDataToken, Method.Post, postJsonObj: createRestrictedDataTokenRequest, cancellationToken: cancellationToken); var response = await ExecuteRequestAsync( RateLimitType.Token_CreateRestrictedDataToken, cancellationToken: cancellationToken); return response; } } }