diff --git a/CryptoExchange.Net/Clients/BaseRestClient.cs b/CryptoExchange.Net/Clients/BaseRestClient.cs index c5d9586..b02357f 100644 --- a/CryptoExchange.Net/Clients/BaseRestClient.cs +++ b/CryptoExchange.Net/Clients/BaseRestClient.cs @@ -61,6 +61,44 @@ namespace CryptoExchange.Net apiClient.SetApiCredentials(credentials); } + /// + /// Execute a request to the uri and returns if it was successful + /// + /// The API client the request is for + /// The uri to send the request to + /// The method of the request + /// Cancellation token + /// The parameters of the request + /// Whether or not the request should be authenticated + /// Where the parameters should be placed, overwrites the value set in the client + /// How array parameters should be serialized, overwrites the value set in the client + /// Credits used for the request + /// The JsonSerializer to use for deserialization + /// Additional headers to send with the request + /// Ignore rate limits for this request + /// + [return: NotNull] + protected virtual async Task SendRequestAsync(RestApiClient apiClient, + Uri uri, + HttpMethod method, + CancellationToken cancellationToken, + Dictionary? parameters = null, + bool signed = false, + HttpMethodParameterPosition? parameterPosition = null, + ArrayParametersSerialization? arraySerialization = null, + int requestWeight = 1, + JsonSerializer? deserializer = null, + Dictionary? additionalHeaders = null, + bool ignoreRatelimit = false) + { + var request = await PrepareRequestAsync(apiClient, uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false); + if (!request) + return new WebCallResult(request.Error!); + + var result = await GetResponseAsync(apiClient, request.Data, deserializer, cancellationToken, true).ConfigureAwait(false); + return result.AsDataless(); + } + /// /// Execute a request to the uri and deserialize the response into the provided type parameter /// @@ -93,6 +131,42 @@ namespace CryptoExchange.Net Dictionary? additionalHeaders = null, bool ignoreRatelimit = false ) where T : class + { + var request = await PrepareRequestAsync(apiClient, uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false); + if (!request) + return new WebCallResult(request.Error!); + + return await GetResponseAsync(apiClient, request.Data, deserializer, cancellationToken, false).ConfigureAwait(false); + } + + /// + /// Prepares a request to be sent to the server + /// + /// The API client the request is for + /// The uri to send the request to + /// The method of the request + /// Cancellation token + /// The parameters of the request + /// Whether or not the request should be authenticated + /// Where the parameters should be placed, overwrites the value set in the client + /// How array parameters should be serialized, overwrites the value set in the client + /// Credits used for the request + /// The JsonSerializer to use for deserialization + /// Additional headers to send with the request + /// Ignore rate limits for this request + /// + protected virtual async Task> PrepareRequestAsync(RestApiClient apiClient, + Uri uri, + HttpMethod method, + CancellationToken cancellationToken, + Dictionary? parameters = null, + bool signed = false, + HttpMethodParameterPosition? parameterPosition = null, + ArrayParametersSerialization? arraySerialization = null, + int requestWeight = 1, + JsonSerializer? deserializer = null, + Dictionary? additionalHeaders = null, + bool ignoreRatelimit = false) { var requestId = NextId(); @@ -102,7 +176,7 @@ namespace CryptoExchange.Net if (!syncTimeResult) { log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error); - return syncTimeResult.As(default); + return syncTimeResult.As(default); } } @@ -112,20 +186,20 @@ namespace CryptoExchange.Net { var limitResult = await limiter.LimitRequestAsync(log, uri.AbsolutePath, method, signed, apiClient.Options.ApiCredentials?.Key, apiClient.Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false); if (!limitResult.Success) - return new WebCallResult(limitResult.Error!); + return new CallResult(limitResult.Error!); } } if (signed && apiClient.AuthenticationProvider == null) { log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided"); - return new WebCallResult(new NoApiCredentialsError()); + return new CallResult(new NoApiCredentialsError()); } log.Write(LogLevel.Information, $"[{requestId}] Creating request for " + uri); var paramsPosition = parameterPosition ?? apiClient.ParameterPositions[method]; var request = ConstructRequest(apiClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? apiClient.arraySerialization, requestId, additionalHeaders); - + string? paramString = ""; if (paramsPosition == HttpMethodParameterPosition.InBody) paramString = $" with request body '{request.Content}'"; @@ -133,12 +207,14 @@ namespace CryptoExchange.Net var headers = request.GetHeaders(); if (headers.Any()) paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")); - + apiClient.TotalRequestsMade++; log.Write(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}"); - return await GetResponseAsync(apiClient, request, deserializer, cancellationToken).ConfigureAwait(false); + return new CallResult(request); } + + /// /// Executes the request and returns the result deserialized into the type parameter class /// @@ -146,8 +222,14 @@ namespace CryptoExchange.Net /// The request object to execute /// The JsonSerializer to use for deserialization /// Cancellation token + /// If an empty response is expected /// - protected virtual async Task> GetResponseAsync(BaseApiClient apiClient, IRequest request, JsonSerializer? deserializer, CancellationToken cancellationToken) + protected virtual async Task> GetResponseAsync( + BaseApiClient apiClient, + IRequest request, + JsonSerializer? deserializer, + CancellationToken cancellationToken, + bool expectedEmptyResponse) { try { @@ -169,22 +251,52 @@ namespace CryptoExchange.Net response.Close(); log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(log.Level == LogLevel.Trace ? (": "+data): "")}"); - // Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example - var parseResult = ValidateJson(data); - if (!parseResult.Success) - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!); + if (!expectedEmptyResponse) + { + // Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example + var parseResult = ValidateJson(data); + if (!parseResult.Success) + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!); - // Let the library implementation see if it is an error response, and if so parse the error - var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false); - if (error != null) - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!); + // Let the library implementation see if it is an error response, and if so parse the error + var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false); + if (error != null) + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!); - // Not an error, so continue deserializing - var deserializeResult = Deserialize(parseResult.Data, deserializer, request.RequestId); - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data: null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error); + // Not an error, so continue deserializing + var deserializeResult = Deserialize(parseResult.Data, deserializer, request.RequestId); + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error); + } + else + { + if (!string.IsNullOrEmpty(data)) + { + var parseResult = ValidateJson(data); + if (!parseResult.Success) + // Not empty, and not json + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!); + + var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false); + if (error != null) + // Error response + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!); + } + + // Empty success response; okay + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default); + } } else { + if (expectedEmptyResponse) + { + // We expected an empty response and the request is successful and don't manually parse errors, so assume it's correct + responseStream.Close(); + response.Close(); + + return new WebCallResult(statusCode, headers, sw.Elapsed, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null); + } + // Success status code, and we don't have to check for errors. Continue deserializing directly from the stream var desResult = await DeserializeAsync(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false); responseStream.Close();