Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions src/Twilio/Clients/BearerToken/TwilioOrgsTokenRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#endif

using Twilio.Http;
using Twilio.Http.BearerToken;

Check warning on line 17 in src/Twilio/Clients/BearerToken/TwilioOrgsTokenRestClient.cs

View workflow job for this annotation

GitHub Actions / Test

The using directive for 'Twilio.Http.BearerToken' appeared previously in this namespace

Check warning on line 17 in src/Twilio/Clients/BearerToken/TwilioOrgsTokenRestClient.cs

View workflow job for this annotation

GitHub Actions / Test

The using directive for 'Twilio.Http.BearerToken' appeared previously in this namespace
#if NET35
using Twilio.Http.Net35;
using System.Collections.Generic;
Expand Down Expand Up @@ -185,14 +185,14 @@

// If 'exp' claim is missing or not a valid timestamp, consider the token expired
throw new ApiConnectionException("token expired 1");
return true;

Check warning on line 188 in src/Twilio/Clients/BearerToken/TwilioOrgsTokenRestClient.cs

View workflow job for this annotation

GitHub Actions / Test

Unreachable code detected
}
catch (Exception ex)
{
// Handle exceptions (e.g., malformed token or invalid JSON)
Console.WriteLine($"Error checking token expiration: {ex.Message}");
throw new ApiConnectionException("token expired 2");
return true; // Consider as expired if there's an error

Check warning on line 195 in src/Twilio/Clients/BearerToken/TwilioOrgsTokenRestClient.cs

View workflow job for this annotation

GitHub Actions / Test

Unreachable code detected
}
}

Expand All @@ -209,7 +209,7 @@
var handler = new JwtSecurityTokenHandler();
try{
var jwtToken = handler.ReadJwtToken(token);
var exp = jwtToken.Payload.Exp;

Check warning on line 212 in src/Twilio/Clients/BearerToken/TwilioOrgsTokenRestClient.cs

View workflow job for this annotation

GitHub Actions / Test

'JwtPayload.Exp' is obsolete: '`int? JwtPayload.Exp` is deprecated and will be removed in a future release. Use `long? JwtPayload.Expiration` instead. For more information, see https://aka.ms/IdentityModel/7-breaking-changes'
if (exp.HasValue)
{
var expirationDate = DateTimeOffset.FromUnixTimeSeconds(exp.Value).UtcDateTime;
Expand Down Expand Up @@ -282,7 +282,6 @@
throw new ApiConnectionException("Connection Error: No response received.");
}


if (response.StatusCode >= HttpStatusCode.OK && response.StatusCode < HttpStatusCode.BadRequest)
{
return response;
Expand All @@ -296,18 +295,40 @@
}
catch (JsonReaderException) { /* Allow null check below to handle */ }

if (restException == null)
if (restException != null)
{
throw new ApiException("Api Error: " + response.StatusCode + " - " + (response.Content ?? "[no content]"));
throw new ApiException(
restException.Code,
(int)response.StatusCode,
restException.Message ?? "Unable to make request, " + response.StatusCode,
restException.MoreInfo,
restException.Details
);
}

throw new ApiException(
restException.Code,
(int)response.StatusCode,
restException.Message ?? "Unable to make request, " + response.StatusCode,
restException.MoreInfo,
restException.Details
);
// Try to deserialize as RFC 9457 format first (RestApiStandardException)
RestApiStandardException restApiStandardException = null;
try
{
restApiStandardException = RestApiStandardException.FromJson(response.Content);
}
catch (JsonReaderException) { /* Allow fallback to legacy format */ }

if (restApiStandardException != null)
{
throw new ApiStandardException(
restApiStandardException.Code,
(int)response.StatusCode,
restApiStandardException.Type,
restApiStandardException.Title,
restApiStandardException.Detail,
restApiStandardException.Instance,
restApiStandardException.Errors
);
}

throw new ApiException("Api Error: " + response.StatusCode + " - " + (response.Content ?? "[no content]"));

}

/// <summary>
Expand Down
43 changes: 34 additions & 9 deletions src/Twilio/Clients/NoAuth/TwilioNoAuthRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,18 +173,43 @@ private static Response ProcessResponse(Response response)
}
catch (JsonReaderException) { /* Allow null check below to handle */ }

if (restException == null)

if (restException != null)
{
throw new ApiException("Api Error: " + response.StatusCode + " - " + (response.Content ?? "[no content]"));
throw new ApiException(
restException.Code,
(int)response.StatusCode,
restException.Message ?? "Unable to make request, " + response.StatusCode,
restException.MoreInfo,
restException.Details
);
}

throw new ApiException(
restException.Code,
(int)response.StatusCode,
restException.Message ?? "Unable to make request, " + response.StatusCode,
restException.MoreInfo,
restException.Details
);

// Try to deserialize as RFC 9457 format first (RestApiStandardException)
RestApiStandardException restApiStandardException = null;
try
{
restApiStandardException = RestApiStandardException.FromJson(response.Content);
}
catch (JsonReaderException) { /* Allow fallback to legacy format */ }

// Check if it's a valid RFC 9457 response (has 'type' field)
if (restApiStandardException != null)
{
throw new ApiStandardException(
restApiStandardException.Code,
(int)response.StatusCode,
restApiStandardException.Type,
restApiStandardException.Title,
restApiStandardException.Detail,
restApiStandardException.Instance,
restApiStandardException.Errors
);
}

// If both RestException and RestApiStandardException are null and throw default exception
throw new ApiException("Api Error: " + response.StatusCode + " - " + (response.Content ?? "[no content]"));
}

/// <summary>
Expand Down
42 changes: 33 additions & 9 deletions src/Twilio/Clients/TwilioRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,18 +246,42 @@ private static Response ProcessResponse(Response response)
}
catch (JsonReaderException) { /* Allow null check below to handle */ }

if (restException == null)
if (restException != null)
{
throw new ApiException("Api Error: " + response.StatusCode + " - " + (response.Content ?? "[no content]"));
throw new ApiException(
restException.Code,
(int)response.StatusCode,
restException.Message ?? "Unable to make request, " + response.StatusCode,
restException.MoreInfo,
restException.Details
);
}


// Try to deserialize as RFC 9457 format first (RestApiStandardException)
RestApiStandardException restApiStandardException = null;
try
{
restApiStandardException = RestApiStandardException.FromJson(response.Content);
}
catch (JsonReaderException) { /* Allow fallback to legacy format */ }

throw new ApiException(
restException.Code,
(int)response.StatusCode,
restException.Message ?? "Unable to make request, " + response.StatusCode,
restException.MoreInfo,
restException.Details
);
// Check if it's a valid RFC 9457 response (has 'type' field)
if (restApiStandardException != null)
{
throw new ApiStandardException(
restApiStandardException.Code,
(int)response.StatusCode,
restApiStandardException.Type,
restApiStandardException.Title,
restApiStandardException.Detail,
restApiStandardException.Instance,
restApiStandardException.Errors
);
}

// If both RestException and RestApiStandardException are null and throw default exception
throw new ApiException("Api Error: " + response.StatusCode + " - " + (response.Content ?? "[no content]"));
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Twilio/Exceptions/ApiException.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;

namespace Twilio.Exceptions
Expand Down
90 changes: 90 additions & 0 deletions src/Twilio/Exceptions/ApiStandardException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;

namespace Twilio.Exceptions
{
/// <summary>
/// RFC 9457 compliant API Exception for HTTP APIs
/// </summary>
public class ApiStandardException : TwilioException
{
/// <summary>
/// Twilio error code
/// </summary>
public int Code { get; }

/// <summary>
/// HTTP status code
/// </summary>
public int Status { get; }

/// <summary>
/// A URI reference identifying the problem type (RFC 9457)
/// </summary>
public string Type { get; }

/// <summary>
/// A short, human-readable summary of the problem type (RFC 9457)
/// </summary>
public string Title { get; }

/// <summary>
/// A human-readable explanation specific to this occurrence of the problem (RFC 9457)
/// </summary>
public string Detail { get; }

/// <summary>
/// A URI reference that identifies the specific occurrence of the problem (RFC 9457)
/// </summary>
public string Instance { get; }

/// <summary>
/// Validation errors for this occurrence (RFC 9457 extension)
/// </summary>
public List<ErrorDetail> Errors { get; }

/// <summary>
/// Create an ApiStandardException with message
/// </summary>
/// <param name="message">Exception message</param>
public ApiStandardException(string message) : base(message) { }

/// <summary>
/// Create an ApiStandardException from another Exception
/// </summary>
/// <param name="message">Exception message</param>
/// <param name="exception">Exception to copy details from</param>
public ApiStandardException(string message, Exception exception) : base(message, exception) { }

/// <summary>
/// Create an ApiStandardException with RFC 9457 fields
/// </summary>
/// <param name="code">Twilio error code</param>
/// <param name="status">HTTP status code</param>
/// <param name="type">URI reference identifying the problem type (RFC 9457)</param>
/// <param name="title">Short summary of the problem type (RFC 9457)</param>
/// <param name="detail">Human-readable explanation specific to this occurrence (RFC 9457)</param>
/// <param name="instance">URI identifying this specific occurrence (RFC 9457)</param>
/// <param name="errors">Validation errors (RFC 9457)</param>
/// <param name="exception">Original exception</param>
public ApiStandardException(
int code,
int status,
string type,
string title,
string detail,
string instance,
List<ErrorDetail> errors,
Exception exception = null
) : base(detail ?? title, exception)
{
Code = code;
Status = status;
Type = type;
Title = title;
Detail = detail;
Instance = instance;
Errors = errors;
}
}
}
94 changes: 94 additions & 0 deletions src/Twilio/Exceptions/RestApiStandardException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using Newtonsoft.Json;
using System.Collections.Generic;

namespace Twilio.Exceptions
{
/// <summary>
/// Represents a single validation error detail (RFC 9457)
/// </summary>
[JsonObject(MemberSerialization.OptIn)]
public class ErrorDetail
{
/// <summary>
/// A human-readable explanation of the validation error for this specific field.
/// </summary>
[JsonProperty("detail")]
public string Detail { get; set; }

/// <summary>
/// A JSON Pointer (RFC 6901) to the location in the request where the error occurred.
/// </summary>
[JsonProperty("pointer")]
public string Pointer { get; set; }
}

/// <summary>
/// RFC 9457 compliant REST API Standard Exception for HTTP APIs
/// </summary>
[JsonObject(MemberSerialization.OptIn)]
public class RestApiStandardException : TwilioException
{
/// <summary>
/// A URI reference identifying the problem type
/// </summary>
/// mandatory
[JsonProperty("type")]
public string Type { get; private set; }

/// <summary>
/// A short, human-readable summary of the problem type
/// </summary>
/// mandatory
[JsonProperty("title")]
public string Title { get; private set; }

/// <summary>
/// The numeric Twilio error code
/// </summary>
/// mandatory
[JsonProperty("code")]
public int Code { get; private set; }

/// <summary>
/// HTTP status code
/// </summary>
/// mandatory
[JsonProperty("status")]
public int Status { get; private set; }

/// <summary>
/// A human-readable explanation specific to this occurrence of the problem
/// </summary>
/// optional
[JsonProperty("detail")]
public string Detail { get; private set; }

/// <summary>
/// A URI reference that identifies the specific occurrence of the problem
/// </summary>
/// optional
[JsonProperty("instance")]
public string Instance { get; private set; }

/// <summary>
/// Validation errors for this occurrence (RFC 9457 extension)
/// </summary>
/// optional
[JsonProperty("errors")]
public List<ErrorDetail> Errors { get; private set; }

/// <summary>
/// Create an empty RestApiStandardException
/// </summary>
public RestApiStandardException() { }

/// <summary>
/// Create a RestApiStandardException from a JSON payload
/// </summary>
/// <param name="json">JSON string to parse</param>
public static RestApiStandardException FromJson(string json)
{
return JsonConvert.DeserializeObject<RestApiStandardException>(json);
}
}
}
Loading
Loading