You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
336 lines
14 KiB
C#
336 lines
14 KiB
C#
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<KeyValuePair<string, string>> LastHeaders { get; set; }
|
|
|
|
protected string ApiBaseUrl => AmazonCredential.Environment == Constants.Environments.Sandbox
|
|
? AmazonSandboxUrl
|
|
: AmazonProductionUrl;
|
|
|
|
public IList<KeyValuePair<string, string>> 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<KeyValuePair<string, string>> 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<T> ExecuteRequestTry<T>(RateLimitType rateLimitType = RateLimitType.UNSET,
|
|
CancellationToken cancellationToken = default) where T : new()
|
|
{
|
|
RestHeader();
|
|
AddAccessToken();
|
|
AddShippingBusinessId();
|
|
var response = await RequestClient.ExecuteAsync<T>(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<T>(response.Content);
|
|
return response.Data;
|
|
}
|
|
|
|
private void SaveLastRequestHeader(IReadOnlyCollection<HeaderParameter> parameters)
|
|
{
|
|
LastHeaders = new List<KeyValuePair<string, string>>();
|
|
foreach (RestSharp.Parameter parameter in parameters ?? Enumerable.Empty<HeaderParameter>())
|
|
if (parameter != null && parameter.Name != null && parameter.Value != null)
|
|
LastHeaders.Add(new KeyValuePair<string, string>(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<T> ExecuteRequestAsync<T>(RateLimitType rateLimitType = RateLimitType.UNSET,
|
|
CancellationToken cancellationToken = default) where T : new()
|
|
{
|
|
var tryCount = 0;
|
|
while (true)
|
|
try
|
|
{
|
|
return await ExecuteRequestTry<T>(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<Parameter> 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<KeyValuePair<string, string>> 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 void AddShippingBusinessId()
|
|
{
|
|
if (AmazonCredential.ShippingBusiness.HasValue)
|
|
Request.AddOrUpdateHeader(ShippingBusinessIdHeaderName,
|
|
AmazonCredential.ShippingBusiness.Value.GetEnumMemberValue());
|
|
}
|
|
|
|
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<CreateRestrictedDataTokenResponse> CreateRestrictedDataTokenAsync(
|
|
CreateRestrictedDataTokenRequest createRestrictedDataTokenRequest,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await CreateAuthorizedRequestAsync(TokenApiUrls.RestrictedDataToken, Method.Post,
|
|
postJsonObj: createRestrictedDataTokenRequest, cancellationToken: cancellationToken);
|
|
var response = await ExecuteRequestAsync<CreateRestrictedDataTokenResponse>(
|
|
RateLimitType.Token_CreateRestrictedDataToken, cancellationToken: cancellationToken);
|
|
return response;
|
|
}
|
|
}
|
|
} |