Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 8 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No

"permissions": {
"allow": [
"Bash(dotnet test:*)",
"Bash(dotnet run:*)"
]
}
}
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