diff --git a/src/AvaTaxClient.cs b/src/AvaTaxClient.cs index 3661b58f..c657aec0 100644 --- a/src/AvaTaxClient.cs +++ b/src/AvaTaxClient.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using System.Threading; using System.Runtime.ExceptionServices; #endif using System.Net; @@ -23,10 +24,15 @@ namespace Avalara.AvaTax.RestClient /// public partial class AvaTaxClient { + private AvaTaxClientOptions _options = new AvaTaxClientOptions(); private Dictionary _clientHeaders = new Dictionary(); private Uri _envUri; #if PORTABLE - private static HttpClient _client = new HttpClient() { Timeout = TimeSpan.FromMinutes(20) }; + private static HttpClient _client = new HttpClient() + { + // This timeout will be the default timeout used for all requests if if a timeout is not provided with the AvaTaxClientOptions + Timeout = TimeSpan.FromMinutes(20) + }; #endif /// @@ -81,9 +87,9 @@ public AvaTaxClient(string appName, string appVersion, string machineName, Uri c WithClientIdentifier(appName, appVersion, machineName); _envUri = customEnvironment; } -#endregion + #endregion -#region Security + #region Security /// /// Sets the default security header string /// @@ -165,9 +171,20 @@ public AvaTaxClient WithClientIdentifier(string appName, string appVersion, stri _clientHeaders.Add(Constants.AVALARA_CLIENT_HEADER, String.Format("{0}; {1}; {2}; {3}; {4}", appName, appVersion, "CSharpRestClient", API_VERSION, machineName)); return this; } -#endregion + #endregion + + /// + /// Sets options for the AvaTaxClient and how it behaves. + /// + /// + /// + public AvaTaxClient WithOptions(AvaTaxClientOptions options) + { + _options = options ?? new AvaTaxClientOptions(); + return this; + } -#region REST Call Interface + #region REST Call Interface #if PORTABLE /// /// Implementation of asynchronous client APIs @@ -236,9 +253,9 @@ public FileResult RestCallFile(string verb, AvaTaxPath relativePath, object payl } } #endif -#endregion + #endregion -#region Implementation + #region Implementation private JsonSerializerSettings _serializer_settings = null; private JsonSerializerSettings SerializerSettings { @@ -354,7 +371,27 @@ private async Task InternalRestCallAsync(CallDuration cd, s // Send cd.FinishSetup(); - return await _client.SendAsync(request).ConfigureAwait(false); + CancellationTokenSource timeoutTokenSource = null; + + try + { + CancellationToken token = default(CancellationToken); + if (_options != null && _options.Timeout.HasValue) + { + timeoutTokenSource = new CancellationTokenSource(_options.Timeout.Value); + token = timeoutTokenSource.Token; + } + + return await _client.SendAsync(request, token).ConfigureAwait(false); + } + finally + { + if (timeoutTokenSource != null) + { + timeoutTokenSource.Dispose(); + timeoutTokenSource = null; + } + } } } @@ -482,7 +519,13 @@ private FileResult RestCallFile(string verb, AvaTaxPath relativePath, object con // Use HttpWebRequest so we can get a decent response var wr = (HttpWebRequest)WebRequest.Create(path); wr.Proxy = null; + + // Default to 20 minutes if a timeout has not been provided in the AvaTaxClientOptions. wr.Timeout = 1200000; + if (_options != null && _options.Timeout.HasValue) + { + wr.Timeout = Convert.ToInt32(Math.Floor(_options.Timeout.Value.TotalMilliseconds)); + } // Add headers foreach (var key in _clientHeaders.Keys) @@ -515,7 +558,7 @@ private FileResult RestCallFile(string verb, AvaTaxPath relativePath, object con using (var inStream = response.GetResponseStream()) { const int BUFFER_SIZE = 1024; var chunks = new List(); - var totalBytes = 0; + var totalBytes = 0; var bytesRead = 0; do @@ -531,7 +574,7 @@ private FileResult RestCallFile(string verb, AvaTaxPath relativePath, object con } totalBytes += bytesRead; } while (bytesRead > 0); - + if(totalBytes <= 0) { throw new IOException("Response contained no data"); } @@ -593,6 +636,11 @@ private string RestCallString(string verb, AvaTaxPath relativePath, object conte wr.Proxy = null; wr.Timeout = 1200000; + if (_options != null && _options.Timeout.HasValue) + { + wr.Timeout = Convert.ToInt32(Math.Floor(_options.Timeout.Value.TotalMilliseconds)); + } + // Add headers foreach (var key in _clientHeaders.Keys) { @@ -655,7 +703,7 @@ private string RestCallString(string verb, AvaTaxPath relativePath, object conte } } - // Catch a web exception + // Catch a web exception } catch (WebException webex) { HttpWebResponse httpWebResponse = webex.Response as HttpWebResponse; if (httpWebResponse != null) { diff --git a/src/AvaTaxClientOptions.cs b/src/AvaTaxClientOptions.cs new file mode 100644 index 00000000..a0f8da24 --- /dev/null +++ b/src/AvaTaxClientOptions.cs @@ -0,0 +1,21 @@ +using System; +#if PORTABLE +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Threading; +#endif + +namespace Avalara.AvaTax.RestClient +{ + /// + /// Configuration options for the + /// + public class AvaTaxClientOptions + { + /// + /// The request timeout. + /// + public TimeSpan? Timeout { get; set; } + } +} diff --git a/src/Avalara.AvaTax.net20.csproj b/src/Avalara.AvaTax.net20.csproj index e7d087bf..699634f5 100644 --- a/src/Avalara.AvaTax.net20.csproj +++ b/src/Avalara.AvaTax.net20.csproj @@ -54,6 +54,7 @@ + diff --git a/src/Avalara.AvaTax.net45.csproj b/src/Avalara.AvaTax.net45.csproj index a48fd62d..66625981 100644 --- a/src/Avalara.AvaTax.net45.csproj +++ b/src/Avalara.AvaTax.net45.csproj @@ -59,6 +59,7 @@ +