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();