From 6dce6d7eb28aa7936db1fecdedeff24eadded2c7 Mon Sep 17 00:00:00 2001
From: box-sdk-build <94016436+box-sdk-build@users.noreply.github.com>
Date: Fri, 1 Mar 2024 12:54:23 +0100
Subject: [PATCH] feat: throw BoxApiError in C# fetch, add additional data to
ResponseInfo (box/box-codegen#439) (#23)
---
.codegen.json | 2 +-
Box.Sdk.Gen/Box/ApiException.cs | 54 ------------------
Box.Sdk.Gen/Box/BoxSdkError.cs | 17 ------
Box.Sdk.Gen/Box/CcgAuth/BoxCcgAuth.cs | 2 +-
.../BoxDeveloperTokenAuth.cs | 2 +-
Box.Sdk.Gen/Box/Errors/BoxApiException.cs | 17 ++++++
Box.Sdk.Gen/Box/Errors/BoxSdkException.cs | 19 +++++++
Box.Sdk.Gen/Box/Errors/RequestInfo.cs | 26 +++++++++
Box.Sdk.Gen/Box/Errors/ResponseInfo.cs | 56 +++++++++++++++++++
Box.Sdk.Gen/Box/JwtAuth/BoxJwtAuth.cs | 4 +-
Box.Sdk.Gen/Box/Oauth/BoxOAuth.cs | 4 +-
Box.Sdk.Gen/Networking/Fetch.cs | 51 +++++++++++++----
12 files changed, 166 insertions(+), 88 deletions(-)
delete mode 100644 Box.Sdk.Gen/Box/ApiException.cs
delete mode 100644 Box.Sdk.Gen/Box/BoxSdkError.cs
create mode 100644 Box.Sdk.Gen/Box/Errors/BoxApiException.cs
create mode 100644 Box.Sdk.Gen/Box/Errors/BoxSdkException.cs
create mode 100644 Box.Sdk.Gen/Box/Errors/RequestInfo.cs
create mode 100644 Box.Sdk.Gen/Box/Errors/ResponseInfo.cs
diff --git a/.codegen.json b/.codegen.json
index 359dd7af..ae0f0c9f 100644
--- a/.codegen.json
+++ b/.codegen.json
@@ -1 +1 @@
-{ "engineHash": "905c6a0", "specHash": "b2f7568", "version": "0.1.0" }
+{ "engineHash": "df5b5de", "specHash": "b2f7568", "version": "0.1.0" }
diff --git a/Box.Sdk.Gen/Box/ApiException.cs b/Box.Sdk.Gen/Box/ApiException.cs
deleted file mode 100644
index 60792b24..00000000
--- a/Box.Sdk.Gen/Box/ApiException.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System;
-
-namespace Fetch
-{
- ///
- /// Class representing exception caused most likely by the Http/s request.
- ///
- public class ApiException : Exception
- {
- ///
- /// Status code of the Http/s response.
- ///
- public int StatusCode { get; }
-
- ///
- /// Http/s response in a string format.
- ///
- public string ApiResponse { get; }
-
- ///
- /// Instantiates a new ApiException with the default message
- ///
- /// Status code returned from the api
- /// Response returned from the api
- public ApiException(int statusCode, string apiResponse)
- : base(BaseErrorMessage(statusCode, apiResponse))
- {
- StatusCode = statusCode;
- ApiResponse = apiResponse;
- }
-
- ///
- /// Instantiates a new ApiException with the provided message
- ///
- /// Status code returned from the api
- /// Response returned from the api
- /// Custom error message
- public ApiException(int statusCode, string apiResponse, string message)
- : base(message)
- {
- StatusCode = statusCode;
- ApiResponse = apiResponse;
- }
- internal static string BuildApiExceptionMessage(int statusCode, string apiResponse, string? message = null)
- {
- return message != null ?
- string.Join(Environment.NewLine, message, BaseErrorMessage(statusCode, apiResponse)) :
- BaseErrorMessage(statusCode, apiResponse);
- }
-
- private static string BaseErrorMessage(int statusCode, string apiResponse)
- => $"Api returned an error status code. Status code: {statusCode}, API response: {apiResponse}";
- }
-}
diff --git a/Box.Sdk.Gen/Box/BoxSdkError.cs b/Box.Sdk.Gen/Box/BoxSdkError.cs
deleted file mode 100644
index f0a6a737..00000000
--- a/Box.Sdk.Gen/Box/BoxSdkError.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System;
-
-namespace Errors
-{
- public class BoxSdkError : Exception {
-
- public System.DateTimeOffset? Timestamp { get; set; } = default;
-
- public string? Error { get; set; } = default;
-
- public string Name { get; set; }
-
- public BoxSdkError(string message, string name = "BoxSdkError") : base(message) {
- Name = name;
- }
- }
-}
\ No newline at end of file
diff --git a/Box.Sdk.Gen/Box/CcgAuth/BoxCcgAuth.cs b/Box.Sdk.Gen/Box/CcgAuth/BoxCcgAuth.cs
index cde63ddf..7e9d585b 100644
--- a/Box.Sdk.Gen/Box/CcgAuth/BoxCcgAuth.cs
+++ b/Box.Sdk.Gen/Box/CcgAuth/BoxCcgAuth.cs
@@ -119,7 +119,7 @@ public BoxCcgAuth AsEnterprise(string enterpriseId, ITokenStorage? tokenStorage
public async System.Threading.Tasks.Task DownscopeTokenAsync(IReadOnlyList scopes, string? resource = null, string? sharedLink = null, NetworkSession? networkSession = null) {
AccessToken? token = await this.TokenStorage.GetAsync().ConfigureAwait(false);
if (token == null) {
- throw new BoxSdkError(message: "No access token is available. Make an API call to retrieve a token before calling this method.");
+ throw new BoxSdkException(message: "No access token is available. Make an API call to retrieve a token before calling this method.");
}
AuthorizationManager authManager = networkSession != null ? new AuthorizationManager(networkSession: networkSession) : new AuthorizationManager();
AccessToken downscopedToken = await authManager.RequestAccessTokenAsync(requestBody: new PostOAuth2Token(grantType: PostOAuth2TokenGrantTypeField.UrnIetfParamsOauthGrantTypeTokenExchange) { SubjectToken = token.AccessTokenField, SubjectTokenType = PostOAuth2TokenSubjectTokenTypeField.UrnIetfParamsOauthTokenTypeAccessToken, Resource = resource, Scope = string.Join(" ", scopes), BoxSharedLink = sharedLink }).ConfigureAwait(false);
diff --git a/Box.Sdk.Gen/Box/DeveloperTokenAuth/BoxDeveloperTokenAuth.cs b/Box.Sdk.Gen/Box/DeveloperTokenAuth/BoxDeveloperTokenAuth.cs
index fa5d9104..c37b3b71 100644
--- a/Box.Sdk.Gen/Box/DeveloperTokenAuth/BoxDeveloperTokenAuth.cs
+++ b/Box.Sdk.Gen/Box/DeveloperTokenAuth/BoxDeveloperTokenAuth.cs
@@ -27,7 +27,7 @@ public async System.Threading.Tasks.Task RetrieveTokenAsync(Network
/// An object to keep network session state
///
public async System.Threading.Tasks.Task RefreshTokenAsync(NetworkSession? networkSession = null) {
- throw new BoxSdkError(message: "Developer token has expired. Please provide a new one.");
+ throw new BoxSdkException(message: "Developer token has expired. Please provide a new one.");
}
public async System.Threading.Tasks.Task RetrieveAuthorizationHeaderAsync(NetworkSession? networkSession = null) {
diff --git a/Box.Sdk.Gen/Box/Errors/BoxApiException.cs b/Box.Sdk.Gen/Box/Errors/BoxApiException.cs
new file mode 100644
index 00000000..210e1dc0
--- /dev/null
+++ b/Box.Sdk.Gen/Box/Errors/BoxApiException.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Errors
+{
+ public class BoxApiException : BoxSdkException
+ {
+ public RequestInfo RequestInfo { get; set; }
+
+ public ResponseInfo ResponseInfo { get; set; }
+
+ public BoxApiException(string message, DateTimeOffset timeStamp, RequestInfo requestInfo, ResponseInfo responseInfo) : base(message, timeStamp, "BoxApiException")
+ {
+ RequestInfo = requestInfo;
+ ResponseInfo = responseInfo;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Box.Sdk.Gen/Box/Errors/BoxSdkException.cs b/Box.Sdk.Gen/Box/Errors/BoxSdkException.cs
new file mode 100644
index 00000000..8418ba20
--- /dev/null
+++ b/Box.Sdk.Gen/Box/Errors/BoxSdkException.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Errors
+{
+ public class BoxSdkException : Exception
+ {
+ public System.DateTimeOffset? Timestamp { get; set; } = default;
+
+ public string? Error { get; set; } = default;
+
+ public string Name { get; set; }
+
+ public BoxSdkException(string message, DateTimeOffset? timeStamp = null, string name = "BoxSdkException") : base(message)
+ {
+ Name = name;
+ Timestamp = timeStamp ?? DateTimeOffset.UtcNow;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Box.Sdk.Gen/Box/Errors/RequestInfo.cs b/Box.Sdk.Gen/Box/Errors/RequestInfo.cs
new file mode 100644
index 00000000..a9ae4e18
--- /dev/null
+++ b/Box.Sdk.Gen/Box/Errors/RequestInfo.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace Errors
+{
+ public class RequestInfo
+ {
+ public string Method { get; set; }
+
+ public string Url { get; set; }
+
+ public IReadOnlyDictionary QueryParams { get; set; }
+
+ public IReadOnlyDictionary Headers { get; set; }
+
+ public string? Body { get; set; } = default;
+
+ public RequestInfo(string method, string? url, IReadOnlyDictionary? queryParams, IReadOnlyDictionary headers)
+ {
+ Method = method;
+ Url = url ?? "";
+ QueryParams = queryParams ?? new ReadOnlyDictionary(new Dictionary());
+ Headers = headers;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Box.Sdk.Gen/Box/Errors/ResponseInfo.cs b/Box.Sdk.Gen/Box/Errors/ResponseInfo.cs
new file mode 100644
index 00000000..5c25e39a
--- /dev/null
+++ b/Box.Sdk.Gen/Box/Errors/ResponseInfo.cs
@@ -0,0 +1,56 @@
+using Serialization.Json;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Errors
+{
+ public class ResponseInfo
+ {
+ public int StatusCode { get; set; }
+
+ public IReadOnlyDictionary Headers { get; set; }
+
+ public SerializedData? Body { get; set; } = default;
+
+ public string? RawBody { get; set; } = default;
+
+ public string? Code { get; set; } = default;
+
+ public Dictionary? ContextInfo { get; set; } = default;
+
+ public string? RequestId { get; set; } = default;
+
+ public string? HelpUrl { get; set; } = default;
+
+ public ResponseInfo(int statusCode, IReadOnlyDictionary headers, SerializedData body, string rawBody,
+ string? code, Dictionary? contextInfo, string? requestId, string? helpUrl)
+ {
+ StatusCode = statusCode;
+ Headers = headers;
+ Body = body;
+ RawBody = rawBody;
+ Code = code;
+ ContextInfo = contextInfo ?? new Dictionary();
+ RequestId = requestId;
+ HelpUrl = helpUrl;
+ }
+ }
+
+ internal class BoxApiExceptionDetails
+ {
+ [JsonPropertyName("code")]
+ public string? Code { get; set; }
+
+ [JsonPropertyName("context_info")]
+ public Dictionary? ContextInfo { get; set; }
+
+ [JsonPropertyName("request_id")]
+ public string? RequestId { get; set; }
+
+ [JsonPropertyName("help_url")]
+ public string? HelpUrl { get; set; }
+
+ public BoxApiExceptionDetails() { }
+ }
+
+}
\ No newline at end of file
diff --git a/Box.Sdk.Gen/Box/JwtAuth/BoxJwtAuth.cs b/Box.Sdk.Gen/Box/JwtAuth/BoxJwtAuth.cs
index 3cf734df..ce8e15d3 100644
--- a/Box.Sdk.Gen/Box/JwtAuth/BoxJwtAuth.cs
+++ b/Box.Sdk.Gen/Box/JwtAuth/BoxJwtAuth.cs
@@ -46,7 +46,7 @@ public BoxJwtAuth(JwtConfig config) {
///
public async System.Threading.Tasks.Task RefreshTokenAsync(NetworkSession? networkSession = null) {
if (Utils.IsBrowser()) {
- throw new BoxSdkError(message: "JWT auth is not supported in browser environment.");
+ throw new BoxSdkException(message: "JWT auth is not supported in browser environment.");
}
JwtAlgorithm alg = this.Config.Algorithm != null ? NullableUtils.Unwrap(this.Config.Algorithm) : JwtAlgorithm.Rs256;
Dictionary claims = new Dictionary() { { "exp", Utils.GetEpochTimeInSeconds() + 30 }, { "box_sub_type", this.SubjectType } };
@@ -132,7 +132,7 @@ public BoxJwtAuth AsEnterprise(string userId, ITokenStorage? tokenStorage = defa
public async System.Threading.Tasks.Task DownscopeTokenAsync(IReadOnlyList scopes, string? resource = null, string? sharedLink = null, NetworkSession? networkSession = null) {
AccessToken? token = await this.TokenStorage.GetAsync().ConfigureAwait(false);
if (token == null) {
- throw new BoxSdkError(message: "No access token is available. Make an API call to retrieve a token before calling this method.");
+ throw new BoxSdkException(message: "No access token is available. Make an API call to retrieve a token before calling this method.");
}
AuthorizationManager authManager = networkSession != null ? new AuthorizationManager(networkSession: networkSession) : new AuthorizationManager();
AccessToken downscopedToken = await authManager.RequestAccessTokenAsync(requestBody: new PostOAuth2Token(grantType: PostOAuth2TokenGrantTypeField.UrnIetfParamsOauthGrantTypeTokenExchange) { SubjectToken = token.AccessTokenField, SubjectTokenType = PostOAuth2TokenSubjectTokenTypeField.UrnIetfParamsOauthTokenTypeAccessToken, Resource = resource, Scope = string.Join(" ", scopes), BoxSharedLink = sharedLink }).ConfigureAwait(false);
diff --git a/Box.Sdk.Gen/Box/Oauth/BoxOAuth.cs b/Box.Sdk.Gen/Box/Oauth/BoxOAuth.cs
index 41b3c8b1..08ca4531 100644
--- a/Box.Sdk.Gen/Box/Oauth/BoxOAuth.cs
+++ b/Box.Sdk.Gen/Box/Oauth/BoxOAuth.cs
@@ -61,7 +61,7 @@ public async System.Threading.Tasks.Task GetTokensAuthorizationCode
public async System.Threading.Tasks.Task RetrieveTokenAsync(NetworkSession? networkSession = null) {
AccessToken? token = await this.TokenStorage.GetAsync().ConfigureAwait(false);
if (token == null) {
- throw new BoxSdkError(message: "Access and refresh tokens not available. Authenticate before making any API call first.");
+ throw new BoxSdkException(message: "Access and refresh tokens not available. Authenticate before making any API call first.");
}
return token;
}
@@ -119,7 +119,7 @@ public async System.Threading.Tasks.Task RevokeTokenAsync(NetworkSession? networ
public async System.Threading.Tasks.Task DownscopeTokenAsync(IReadOnlyList scopes, string? resource = null, string? sharedLink = null, NetworkSession? networkSession = null) {
AccessToken? token = await this.TokenStorage.GetAsync().ConfigureAwait(false);
if (token == null || token.AccessTokenField == null) {
- throw new BoxSdkError(message: "No access token is available.");
+ throw new BoxSdkException(message: "No access token is available.");
}
AuthorizationManager authManager = networkSession != null ? new AuthorizationManager(networkSession: networkSession) : new AuthorizationManager();
AccessToken downscopedToken = await authManager.RequestAccessTokenAsync(requestBody: new PostOAuth2Token(grantType: PostOAuth2TokenGrantTypeField.UrnIetfParamsOauthGrantTypeTokenExchange) { SubjectToken = token.AccessTokenField, SubjectTokenType = PostOAuth2TokenSubjectTokenTypeField.UrnIetfParamsOauthTokenTypeAccessToken, Scope = string.Join(" ", scopes), Resource = resource, BoxSharedLink = sharedLink }).ConfigureAwait(false);
diff --git a/Box.Sdk.Gen/Networking/Fetch.cs b/Box.Sdk.Gen/Networking/Fetch.cs
index f989d249..024a2f60 100644
--- a/Box.Sdk.Gen/Networking/Fetch.cs
+++ b/Box.Sdk.Gen/Networking/Fetch.cs
@@ -1,4 +1,5 @@
using Box.Sdk.Gen;
+using Errors;
using Microsoft.Extensions.DependencyInjection;
using Serializer;
using System;
@@ -31,7 +32,7 @@ static HttpClientAdapter()
var httpClientFactory = serviceProvider.GetService();
if (httpClientFactory == null)
{
- throw new ArgumentException("Unable to create HttpClient. Cannot get an IHttpClientFactory instance from a ServiceProvider.");
+ throw new BoxSdkException("Unable to create HttpClient. Cannot get an IHttpClientFactory instance from a ServiceProvider.");
}
_clientFactory = httpClientFactory;
}
@@ -67,7 +68,7 @@ public static async Task FetchAsync(string resource, FetchOptions
if (attempt >= networkSession.RetryAttempts)
{
- throw await BuildApiException(response, statusCode, cancellationToken, "Max retry attempts excedeed.").ConfigureAwait(false);
+ throw await BuildApiException(request, response, options, statusCode, cancellationToken, "Max retry attempts excedeed.").ConfigureAwait(false);
}
if (statusCode == 401)
@@ -87,7 +88,7 @@ public static async Task FetchAsync(string resource, FetchOptions
}
else
{
- throw await BuildApiException(response, statusCode, cancellationToken).ConfigureAwait(false);
+ throw await BuildApiException(request, response, options, statusCode, cancellationToken).ConfigureAwait(false);
}
response?.Dispose();
@@ -96,13 +97,43 @@ public static async Task FetchAsync(string resource, FetchOptions
}
- private static async Task BuildApiException(HttpResponseMessage? response, int statusCode, System.Threading.CancellationToken cancellationToken, string? message = null)
+ private static async Task BuildApiException(HttpRequestMessage request, HttpResponseMessage? response, FetchOptions options,
+ int statusCode, System.Threading.CancellationToken cancellationToken, string? message = null)
{
- var responseContent = response != null ? await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false) : "empty";
+ if (message != null)
+ {
+ return new BoxSdkException(message, DateTimeOffset.UtcNow);
+ }
+
+ string responseContent;
+ Dictionary responseHeaders;
+
+ if (response != null)
+ {
+ responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+ responseHeaders = response.Headers.Where(x => x.Value.Any()).ToDictionary(x => x.Key, x => x.Value.First());
+ }
+ else
+ {
+ responseContent = "empty";
+ responseHeaders = new Dictionary();
+ }
+
response?.Dispose();
- return message != null ?
- new ApiException(statusCode, responseContent, ApiException.BuildApiExceptionMessage(statusCode, responseContent, message)) :
- new ApiException(statusCode, responseContent);
+
+ var requestHeaders = request.Headers
+ .Where(x => x.Value.Any())
+ .ToDictionary(x => x.Key, x => x.Value.First());
+
+ var requestInfo = new RequestInfo(request.Method.ToString(), request.RequestUri?.ToString(), options.Parameters, requestHeaders);
+
+ var responseAsSerializedData = JsonUtils.JsonToSerializedData(responseContent);
+ var errorDetails = SimpleJsonSerializer.Deserialize(responseAsSerializedData);
+
+ var responseInfo = new ResponseInfo(statusCode, responseHeaders, responseAsSerializedData, responseContent, errorDetails.Code, errorDetails.ContextInfo,
+ errorDetails.RequestId, errorDetails.HelpUrl);
+
+ return new BoxApiException(responseContent, DateTimeOffset.UtcNow, requestInfo, responseInfo);
}
private static async Task BuildHttpRequest(string resource, FetchOptions options)
@@ -177,7 +208,7 @@ private static async Task ExecuteRequest(HttpClient client,
if (options.MultipartData == null)
{
- throw new ArgumentException("Could not upload file. MultipartData on FetchOptions is null");
+ throw new BoxSdkException("Could not upload file. MultipartData on FetchOptions is null");
}
foreach (var part in options.MultipartData)
@@ -186,7 +217,7 @@ private static async Task ExecuteRequest(HttpClient client,
new StreamContent(part.FileStream) :
part.Data != null ?
new StringContent(JsonUtils.SdToJson(part.Data)) :
- throw new ArgumentException($"HttpContent for MultipartData {part} not found");
+ throw new BoxSdkException($"HttpContent for MultipartData {part} not found");
// for avatar upload
if (part.ContentType != null && part.FileName != null)