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
37 changes: 18 additions & 19 deletions Xero.NetStandard.OAuth2Client/src/Client/XeroClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,7 @@ public async Task<IXeroToken> RefreshAccessTokenAsync(IXeroToken xeroToken)
RefreshToken = xeroToken.RefreshToken
});

if (response.IsError)
{
throw new Exception(response.Error);
}
ThrowIfError(response);

xeroToken.AccessToken = response.AccessToken;
xeroToken.RefreshToken = response.RefreshToken;
Expand All @@ -177,10 +174,7 @@ public async Task<IXeroToken> RequestClientCredentialsTokenAsync(bool fetchTenan
Scope = xeroConfiguration.Scope
});

if (response.IsError)
{
throw new Exception(response.Error);
}
ThrowIfError(response);

var xeroToken = new XeroOAuth2Token()
{
Expand Down Expand Up @@ -215,10 +209,7 @@ public async Task<IXeroToken> RequestAccessTokenAsync(string code)
}
});

if (response.IsError)
{
throw new Exception(response.Error);
}
ThrowIfError(response);

var xeroToken = new XeroOAuth2Token()
{
Expand Down Expand Up @@ -258,11 +249,8 @@ public async Task<IXeroToken> RequestAccessTokenPkceAsync(string code, string co
CodeVerifier = codeVerifier
});

if (response.IsError)
{
throw new Exception(response.Error);
}

ThrowIfError(response);

return new XeroOAuth2Token()
{
AccessToken = response.AccessToken,
Expand Down Expand Up @@ -351,12 +339,23 @@ public async Task RevokeAccessTokenAsync(IXeroToken xeroToken)
Token = xeroToken.RefreshToken
});

ThrowIfError(response);
}

private static void ThrowIfError(TokenResponse response)
{
if (response.IsError)
{
throw new Exception(response.Error);
throw new XeroTokenException(response, response.ErrorDescription);
}
}

return;
private static void ThrowIfError(TokenRevocationResponse response)
{
if (response.IsError)
{
throw new XeroTokenException(response);
}
}
}
}
57 changes: 57 additions & 0 deletions Xero.NetStandard.OAuth2Client/src/Client/XeroTokenException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Net;
using IdentityModel.Client;

namespace Xero.NetStandard.OAuth2.Client
{
/// <summary>
/// Thrown when a call to Xero's OAuth 2.0 token endpoint (token request, refresh, or revocation)
/// fails. Unlike a bare <see cref="Exception"/>, it keeps the OAuth error code, description, and
/// <see cref="ErrorType"/> as structured fields, and preserves the original transport/network
/// exception (if any) as <see cref="Exception.InnerException"/> instead of discarding it.
/// </summary>
/// <remarks>
/// <see cref="ErrorType"/> tells callers whether <see cref="Error"/> is safe to compare against
/// spec-defined OAuth codes (e.g. "invalid_grant"): that's only true for
/// <see cref="ResponseErrorType.Protocol"/>. For <see cref="ResponseErrorType.Http"/> or
/// <see cref="ResponseErrorType.Exception"/>, <see cref="Error"/> is an HTTP reason phrase or a
/// transport exception's message and can never equal a real OAuth code.
///
/// <see cref="Exception.Message"/> is deliberately always just <see cref="Error"/> — never
/// <see cref="Error"/> plus <see cref="ErrorDescription"/> concatenated — so consumers that
/// classify failures by comparing <c>Message</c> against known OAuth codes keep working
/// unchanged whether or not Xero happens to supply a description. Consumers that want the
/// description should read <see cref="ErrorDescription"/> directly.
/// </remarks>
public class XeroTokenException : Exception
{
/// <summary>
/// The OAuth error code (e.g. "invalid_grant") when <see cref="ErrorType"/> is
/// <see cref="ResponseErrorType.Protocol"/>; otherwise an HTTP reason phrase or the underlying
/// exception's message.
/// </summary>
public string Error { get; }

/// <summary>
/// The OAuth <c>error_description</c> supplied by the token endpoint, if any. Always null for
/// token revocation failures (the revocation endpoint's response carries no description) and
/// for non-protocol failures.
/// </summary>
public string ErrorDescription { get; }

/// <summary>Distinguishes a genuine OAuth protocol error from an HTTP-level or transport-level failure.</summary>
public ResponseErrorType ErrorType { get; }

/// <summary>The HTTP status code of the failed response, or 0 if the failure never reached the HTTP layer.</summary>
public HttpStatusCode HttpStatusCode { get; }

public XeroTokenException(ProtocolResponse response, string errorDescription = null)
: base(response?.Error, response?.Exception)
{
Error = response?.Error;
ErrorDescription = errorDescription;
ErrorType = response?.ErrorType ?? ResponseErrorType.None;
HttpStatusCode = response?.HttpStatusCode ?? default;
}
}
}