diff --git a/README.md b/README.md index 66d7318..2daa20b 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,42 @@ Define the timeout for one request ```cs request.WithTimeout(TimeSpan.FromSeconds(100)); ``` +### Allow non http 2xx responses + +#### Globaly + +Allow any status codes : +```cs +client.Settings.HttpStatusCodeAllowed.AllowAnyStatus = true; +``` + +Allow only a range of http status codes : +```cs +client.Settings.HttpStatusCodeAllowed.Add(new HttpStatusRange(400, 420)); +``` + +or + +```cs +client.Settings.HttpStatusCodeAllowed.Add(new HttpStatusRange(System.Net.HttpStatusCode.BadRequest, System.Net.HttpStatusCode.BadGateway)); +``` + +#### By request + +Allow all status code : +```cs +request.AllowAllHttpStatusCode().ExecuteAsync(); +``` + +Allow only a range of http status codes : +```cs +request.AllowRangeHttpStatusCode(400, 420).ExecuteAsync(); +``` + +Allow only on stats code of http status codes : +```cs +request.AllowSpecificHttpStatusCode(409).ExecuteAsync(); +``` ### Download file ```cs diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 4be9cbc..9b5d0c8 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,4 +1,46 @@ # Release notes +# 1.7.0 +* Add possibility for request with content (like POST, PUT ...) to use methods : WithBasicAuthentication WithOAuthBearer WithTimeout WithETagContainer. +* Allow non 2xx response (see sample below) + +## Allow non http 2xx responses + +#### Globaly + +Allow any status codes : +```cs +client.Settings.HttpStatusCodeAllowed.AllowAnyStatus = true; +``` + +Allow only a range of http status codes : +```cs +client.Settings.HttpStatusCodeAllowed.Add(new HttpStatusRange(400, 420)); +``` + +or + +```cs +client.Settings.HttpStatusCodeAllowed.Add(new HttpStatusRange(System.Net.HttpStatusCode.BadRequest, System.Net.HttpStatusCode.BadGateway)); +``` + +#### By request + +Allow all status code : +```cs +request.AllowAllHttpStatusCode().ExecuteAsync(); +``` + +Allow only a range of http status codes : +```cs +request.AllowRangeHttpStatusCode(400, 420).ExecuteAsync(); +``` + +Allow only on stats code of http status codes : +```cs +request.AllowSpecificHttpStatusCode(409).ExecuteAsync(); +``` + + # 1.6.6 * Add icon and license in nuget package diff --git a/Tests/Tiny.RestClient.ForTest.Api/Controllers/GetTestController.cs b/Tests/Tiny.RestClient.ForTest.Api/Controllers/GetTestController.cs index 317e457..9cfaf38 100644 --- a/Tests/Tiny.RestClient.ForTest.Api/Controllers/GetTestController.cs +++ b/Tests/Tiny.RestClient.ForTest.Api/Controllers/GetTestController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.IO; @@ -14,6 +15,18 @@ public GetTestController() { } + [HttpGet("Status500Response")] + public IActionResult Status500Response() + { + return StatusCode(StatusCodes.Status500InternalServerError, new string[] { "value1", "value2" }); + } + + [HttpGet("Status409Response")] + public IActionResult Status409Response() + { + return StatusCode(StatusCodes.Status409Conflict, new string[] { "value1", "value2" }); + } + [HttpGet("NoResponse")] public Task NoResponse() { diff --git a/Tests/Tiny.RestClient.Tests/StatusRangeTests.cs b/Tests/Tiny.RestClient.Tests/StatusRangeTests.cs new file mode 100644 index 0000000..35c85dd --- /dev/null +++ b/Tests/Tiny.RestClient.Tests/StatusRangeTests.cs @@ -0,0 +1,114 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Tiny.RestClient.Tests +{ + [TestClass] + public class StatusRangeTests : BaseTest + { + #region Client scope + [ExpectedException(typeof(HttpException))] + [TestMethod] + public async Task GetErrorWhenCallApiWhenError500() + { + var client = GetClient(); + + try + { + var response = await client. + GetRequest("GetTest/Status500Response"). + ExecuteAsync>(); + } + catch (HttpException ex) + { + Assert.AreEqual(System.Net.HttpStatusCode.InternalServerError, ex.StatusCode); + + throw ex; + } + } + + [TestMethod] + public async Task GetAnyStatusResponseAllowed() + { + var client = GetNewClient(); + client.Settings.HttpStatusCodeAllowed.AllowAnyStatus = true; + + var response = await client. + GetRequest("GetTest/Status500Response"). + ExecuteAsync>(); + Assert.IsNotNull(response); + } + + [TestMethod] + public async Task GetRangeOfStatusesAllowed() + { + var client = GetNewClient(); + client.Settings.HttpStatusCodeAllowed.Add( + new HttpStatusRange( + System.Net.HttpStatusCode.BadRequest, // 400 + System.Net.HttpStatusCode.BadGateway)); // 502 + var response = await client. + GetRequest("GetTest/Status409Response"). + ExecuteAsync>(); + Assert.IsNotNull(response); + } + + [TestMethod] + public async Task GetSpecificStatusResponseAllowed() + { + var client = GetNewClient(); + client.Settings.HttpStatusCodeAllowed.Add(new HttpStatusRange(System.Net.HttpStatusCode.Conflict)); + var response = await client. + GetRequest("GetTest/Status409Response"). + ExecuteAsync>(); + Assert.IsNotNull(response); + } + + [ExpectedException(typeof(ArgumentException))] + [TestMethod] + public void AddInvalidStatusRange() + { + var client = GetNewClient(); + client.Settings.HttpStatusCodeAllowed.Add(new HttpStatusRange(500, 400)); + } + #endregion + + #region Request scope + [TestMethod] + public async Task ForRequest_GetAnyStatusResponseAllowed() + { + var client = GetClient(); + var response = await client. + GetRequest("GetTest/Status500Response"). + AllowAnyHttpStatusCode(). + ExecuteAsync>(); + Assert.IsNotNull(response); + } + + [TestMethod] + public async Task ForRequest_GetRangeOfStatusesResponseAllowed() + { + var client = GetClient(); + var response = await client. + GetRequest("GetTest/Status409Response"). + AllowRangeHttpStatusCode(System.Net.HttpStatusCode.BadRequest, System.Net.HttpStatusCode.BadGateway). + ExecuteAsync>(); + Assert.IsNotNull(response); + } + + [TestMethod] + public async Task ForRequest_GetSpecificStatusResponseAllowed() + { + var client = GetClient(); + var response = await client. + GetRequest("GetTest/Status409Response"). + AllowSpecificHttpStatusCode(System.Net.HttpStatusCode.Conflict). + ExecuteAsync>(); + Assert.IsNotNull(response); + } + #endregion + } +} diff --git a/Tiny.RestClient/HttpStatusRange/HttpStatusRange.cs b/Tiny.RestClient/HttpStatusRange/HttpStatusRange.cs new file mode 100644 index 0000000..fc5c64b --- /dev/null +++ b/Tiny.RestClient/HttpStatusRange/HttpStatusRange.cs @@ -0,0 +1,64 @@ +using System; +using System.Net; + +namespace Tiny.RestClient +{ + /// + /// Represent a range of http code. + /// + public class HttpStatusRange + { + /// + /// Contruct a status range. + /// + /// min status range. + /// max status range. + public HttpStatusRange(HttpStatusCode minHttpStatus, HttpStatusCode maxHttpStatus) + : this((int)minHttpStatus, (int)maxHttpStatus) + { + } + + /// + /// Contruct a status range. + /// + /// min status range. + /// max status range. + public HttpStatusRange(int minHttpStatus, int maxHttpStatus) + { + MinHttpStatus = minHttpStatus; + MaxHttpStatus = maxHttpStatus; + if (maxHttpStatus < minHttpStatus) + { + throw new ArgumentException($"{nameof(maxHttpStatus)} must be superior or egual to {nameof(minHttpStatus)}"); + } + } + + /// + /// Contruct a status range. + /// + /// httpStatus allowed. + public HttpStatusRange(HttpStatusCode httpStatusAllowed) + : this((int)httpStatusAllowed) + { + } + + /// + /// Contruct a status range. + /// + /// httpStatus allowed. + public HttpStatusRange(int httpStatusAllowed) + : this(httpStatusAllowed, httpStatusAllowed) + { + } + + /// + /// Min http status. + /// + public int MinHttpStatus { get; private set; } + + /// + /// MAx http status. + /// + public int MaxHttpStatus { get; private set; } + } +} \ No newline at end of file diff --git a/Tiny.RestClient/HttpStatusRange/HttpStatusRanges.cs b/Tiny.RestClient/HttpStatusRange/HttpStatusRanges.cs new file mode 100644 index 0000000..f54ef56 --- /dev/null +++ b/Tiny.RestClient/HttpStatusRange/HttpStatusRanges.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Tiny.RestClient +{ + /// + /// Represent a collection ofstatus range. + /// + public class HttpStatusRanges : List + { + internal HttpStatusRanges() + { + } + + /// + /// Allow all statuses codes. + /// + public bool AllowAnyStatus { get; set; } + + /// + /// Check if httpStatus is allowed. + /// + /// status code to check. + /// + public bool CheckIfHttpStatusIsAllowed(int statusCode) + { + if (AllowAnyStatus) + { + return true; + } + + return this.Any(r => r.MinHttpStatus <= statusCode && r.MaxHttpStatus >= statusCode); + } + } +} \ No newline at end of file diff --git a/Tiny.RestClient/Request/IParameterRequest.cs b/Tiny.RestClient/Request/IParameterRequest.cs index 2e2eda4..e5db6d9 100644 --- a/Tiny.RestClient/Request/IParameterRequest.cs +++ b/Tiny.RestClient/Request/IParameterRequest.cs @@ -1,4 +1,7 @@ -namespace Tiny.RestClient +using System; +using System.Net; + +namespace Tiny.RestClient { /// /// Interface IParameterRequest. @@ -6,6 +9,71 @@ /// public interface IParameterRequest : IExecutableRequest { + /// + /// Allow any status code (error 500) don't throw exception. + /// + /// The current request. + IParameterRequest AllowAnyHttpStatusCode(); + + /// + /// Allow a range of status code. By default all 2** statuses don't throw exceptions. + /// + /// min http status. + /// max http status. + /// The current request. + IParameterRequest AllowRangeHttpStatusCode(HttpStatusCode minHttpStatus, HttpStatusCode maxHttpStatus); + + /// + /// Allow a range of status code. By default all 2** statuses don't throw exceptions. + /// + /// min http status. + /// max http status. + /// The current request. + IParameterRequest AllowRangeHttpStatusCode(int minHttpStatus, int maxHttpStatus); + + /// + /// Allow a specific status code. + /// + /// status code to be allowed. + /// The current request. + IParameterRequest AllowSpecificHttpStatusCode(HttpStatusCode statusCode); + + /// + /// Allow a specific status code. + /// + /// status code to be allowed. + /// The current request. + IParameterRequest AllowSpecificHttpStatusCode(int statusCode); + + /// + /// Add a basic authentication credentials. + /// + /// the username. + /// the password. + /// The current request. + IParameterRequest WithBasicAuthentication(string username, string password); + + /// + /// Add a bearer token in the request headers. + /// + /// token value. + /// The current request. + IParameterRequest WithOAuthBearer(string token); + + /// + /// With timeout for current request. + /// + /// timeout. + /// The current request. + IParameterRequest WithTimeout(TimeSpan timeout); + + /// + /// With a specific etag container. + /// + /// the eTag container. + /// + IParameterRequest WithETagContainer(IETagContainer eTagContainer); + /// /// Fill header of response. /// diff --git a/Tiny.RestClient/Request/IRequest.cs b/Tiny.RestClient/Request/IRequest.cs index 44dd5fe..52a2613 100644 --- a/Tiny.RestClient/Request/IRequest.cs +++ b/Tiny.RestClient/Request/IRequest.cs @@ -10,35 +10,6 @@ namespace Tiny.RestClient /// public interface IRequest : IExecutableRequest, IFormRequest { - /// - /// Add a basic authentication credentials. - /// - /// the username. - /// the password. - /// The current request. - IRequest WithBasicAuthentication(string username, string password); - - /// - /// Add a bearer token in the request headers. - /// - /// token value. - /// The current request. - IRequest WithOAuthBearer(string token); - - /// - /// With timeout for current request. - /// - /// timeout. - /// The current request. - IRequest WithTimeout(TimeSpan timeout); - - /// - /// With a specific etag container. - /// - /// the eTag container. - /// - IRequest WithETagContainer(IETagContainer eTagContainer); - /// /// Adds the content. /// diff --git a/Tiny.RestClient/Request/Request.cs b/Tiny.RestClient/Request/Request.cs index b25e32b..f692f64 100644 --- a/Tiny.RestClient/Request/Request.cs +++ b/Tiny.RestClient/Request/Request.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -22,13 +23,14 @@ internal class Request : private readonly TinyRestClient _client; private readonly string _route; private Headers _headers; + private Headers _responseHeaders; private Dictionary _queryParameters; private IContent _content; + private IETagContainer _eTagContainer; private List> _formParameters; private MultipartContent _multiPartFormData; - private Headers _responseHeaders; private TimeSpan? _timeout; - private IETagContainer _eTagContainer; + private HttpStatusRanges _httpStatusCodeAllowed; internal HttpMethod HttpMethod { get => _httpMethod; } internal Dictionary QueryParameters { get => _queryParameters; } @@ -38,6 +40,7 @@ internal class Request : internal Headers ResponseHeaders { get => _responseHeaders; } internal Headers Headers { get => _headers; } internal TimeSpan? Timeout { get => _timeout; } + internal HttpStatusRanges HttpStatusCodeAllowed { get => _httpStatusCodeAllowed; } static Request() { @@ -149,7 +152,7 @@ public IParameterRequest AddHeader(string key, string value) } /// - public IRequest WithBasicAuthentication(string username, string password) + public IParameterRequest WithBasicAuthentication(string username, string password) { if (_headers == null) { @@ -161,7 +164,7 @@ public IRequest WithBasicAuthentication(string username, string password) } /// - public IRequest WithOAuthBearer(string token) + public IParameterRequest WithOAuthBearer(string token) { if (_headers == null) { @@ -346,14 +349,14 @@ public IParameterRequest AddQueryParameter(string key, long? value) #endregion /// - public IRequest WithETagContainer(IETagContainer eTagContainer) + public IParameterRequest WithETagContainer(IETagContainer eTagContainer) { _eTagContainer = eTagContainer; return this; } /// - public IRequest WithTimeout(TimeSpan timeout) + public IParameterRequest WithTimeout(TimeSpan timeout) { _timeout = timeout; @@ -493,6 +496,49 @@ IMultiPartFromDataExecutableRequest IMultipartFromDataRequest.AddContent @@ -86,6 +87,11 @@ public Headers DefaultHeaders /// public Compressions Compressions { get; private set; } + /// + /// Range of status allowed if empty use default behavior. + /// + public HttpStatusRanges HttpStatusCodeAllowed { get; private set; } + /// /// Gets or set the handler used when HttpException will be throw (can be used to transform exception). /// diff --git a/Tiny.RestClient/Tiny.RestClient.csproj b/Tiny.RestClient/Tiny.RestClient.csproj index 4fcda7e..b327e75 100644 --- a/Tiny.RestClient/Tiny.RestClient.csproj +++ b/Tiny.RestClient/Tiny.RestClient.csproj @@ -1,7 +1,7 @@  netstandard1.1;netstandard1.2;netstandard1.3;netstandard2.0;net45;net46;net47 - 1.6.6 + 1.7.0 Copyright © Jérôme Giacomini 2019 en Tiny.RestClient @@ -47,7 +47,7 @@ True - + latest diff --git a/Tiny.RestClient/TinyRestClient.cs b/Tiny.RestClient/TinyRestClient.cs index e72d7c1..779bf58 100644 --- a/Tiny.RestClient/TinyRestClient.cs +++ b/Tiny.RestClient/TinyRestClient.cs @@ -219,7 +219,7 @@ internal async Task ExecuteAsync( using (HttpResponseMessage response = await SendRequestAsync(tinyRequest.HttpMethod, requestUri, tinyRequest.Headers, content, eTagContainer, formatter, tinyRequest.Timeout, cancellationToken).ConfigureAwait(false)) { - using (var stream = await ReadResponseAsync(response, tinyRequest.ResponseHeaders, eTagContainer, cancellationToken).ConfigureAwait(false)) + using (var stream = await ReadResponseAsync(response, tinyRequest.ResponseHeaders, tinyRequest.HttpStatusCodeAllowed, eTagContainer, cancellationToken).ConfigureAwait(false)) { if (stream == null || stream.CanRead == false) { @@ -279,10 +279,11 @@ internal async Task ExecuteAsync( { using (var content = await CreateContentAsync(tinyRequest.Content, cancellationToken).ConfigureAwait(false)) { + var etagContainer = GetETagContainer(tinyRequest); var requestUri = BuildRequestUri(tinyRequest.Route, tinyRequest.QueryParameters); - using (HttpResponseMessage response = await SendRequestAsync(tinyRequest.HttpMethod, requestUri, tinyRequest.Headers, content, null, null, tinyRequest.Timeout, cancellationToken).ConfigureAwait(false)) + using (HttpResponseMessage response = await SendRequestAsync(tinyRequest.HttpMethod, requestUri, tinyRequest.Headers, content, etagContainer, null, tinyRequest.Timeout, cancellationToken).ConfigureAwait(false)) { - await HandleResponseAsync(response, tinyRequest.ResponseHeaders, null, cancellationToken).ConfigureAwait(false); + await HandleResponseAsync(response, tinyRequest.ResponseHeaders, tinyRequest.HttpStatusCodeAllowed, etagContainer, cancellationToken).ConfigureAwait(false); } } } @@ -298,7 +299,7 @@ internal async Task ExecuteAsByteArrayResultAsync( using (HttpResponseMessage response = await SendRequestAsync(tinyRequest.HttpMethod, requestUri, tinyRequest.Headers, content, eTagContainer, null, tinyRequest.Timeout, cancellationToken).ConfigureAwait(false)) { - using (var stream = await ReadResponseAsync(response, tinyRequest.ResponseHeaders, eTagContainer, cancellationToken).ConfigureAwait(false)) + using (var stream = await ReadResponseAsync(response, tinyRequest.ResponseHeaders, tinyRequest.HttpStatusCodeAllowed, eTagContainer, cancellationToken).ConfigureAwait(false)) { if (stream == null || !stream.CanRead) { @@ -325,7 +326,7 @@ internal async Task ExecuteAsStreamResultAsync( var requestUri = BuildRequestUri(tinyRequest.Route, tinyRequest.QueryParameters); var eTagContainer = GetETagContainer(tinyRequest); var response = await SendRequestAsync(tinyRequest.HttpMethod, requestUri, tinyRequest.Headers, content, eTagContainer, null, tinyRequest.Timeout, cancellationToken).ConfigureAwait(false); - var stream = await ReadResponseAsync(response, tinyRequest.ResponseHeaders, eTagContainer, cancellationToken).ConfigureAwait(false); + var stream = await ReadResponseAsync(response, tinyRequest.ResponseHeaders, tinyRequest.HttpStatusCodeAllowed, eTagContainer, cancellationToken).ConfigureAwait(false); if (stream == null || !stream.CanRead) { return null; @@ -345,7 +346,7 @@ internal async Task ExecuteAsStringResultAsync( var eTagContainer = GetETagContainer(tinyRequest); using (var response = await SendRequestAsync(tinyRequest.HttpMethod, requestUri, tinyRequest.Headers, content, eTagContainer, null, tinyRequest.Timeout, cancellationToken).ConfigureAwait(false)) { - var stream = await ReadResponseAsync(response, tinyRequest.ResponseHeaders, eTagContainer, cancellationToken).ConfigureAwait(false); + var stream = await ReadResponseAsync(response, tinyRequest.ResponseHeaders, tinyRequest.HttpStatusCodeAllowed, GetETagContainer(tinyRequest), cancellationToken).ConfigureAwait(false); if (stream == null || !stream.CanRead) { return null; @@ -740,10 +741,11 @@ private IETagContainer GetETagContainer(Request request) private async Task ReadResponseAsync( HttpResponseMessage response, Headers responseHeader, + HttpStatusRanges httpStatusRanges, IETagContainer eTagContainer, CancellationToken cancellationToken) { - await HandleResponseAsync(response, responseHeader, eTagContainer, cancellationToken).ConfigureAwait(false); + await HandleResponseAsync(response, responseHeader, httpStatusRanges, eTagContainer, cancellationToken).ConfigureAwait(false); Stream stream; if (eTagContainer != null && response.StatusCode == HttpStatusCode.NotModified) { @@ -789,6 +791,7 @@ private async Task DecompressAsync(HttpResponseMessage response, Stream private async Task HandleResponseAsync( HttpResponseMessage response, Headers responseHeaders, + HttpStatusRanges requestAllowedStatusRange, IETagContainer eTagContainer, CancellationToken cancellationToken) { @@ -810,13 +813,22 @@ private async Task HandleResponseAsync( return; } + bool haveToEnsureSuccessStatusCode = false; if (!response.IsSuccessStatusCode) { - content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); + // if status code is not allowed we read the content + if (!CheckIfStatusCodeIsAllowed((int)response.StatusCode, requestAllowedStatusRange)) + { + haveToEnsureSuccessStatusCode = true; + content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + } } - response.EnsureSuccessStatusCode(); + if (haveToEnsureSuccessStatusCode) + { + response.EnsureSuccessStatusCode(); + } } catch (OperationCanceledException ex) { @@ -843,6 +855,31 @@ private async Task HandleResponseAsync( throw newEx; } } + + private bool CheckIfStatusCodeIsAllowed(int statusCode, HttpStatusRanges requestAllowedStatusRange) + { + if (requestAllowedStatusRange != null) + { + if (!requestAllowedStatusRange.CheckIfHttpStatusIsAllowed(statusCode)) + { + if (!Settings.HttpStatusCodeAllowed.CheckIfHttpStatusIsAllowed(statusCode)) + { + return false; + } + } + + return true; + } + else + { + if (!Settings.HttpStatusCodeAllowed.CheckIfHttpStatusIsAllowed(statusCode)) + { + return false; + } + + return true; + } + } #endregion } } \ No newline at end of file