diff --git a/Directory.Build.props b/Directory.Build.props index a6b9c51b..2def6a21 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,7 @@ + latest + git https://github.com/ThreeMammals/Ocelot diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index 58c18901..6d583e66 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -1,15 +1,15 @@ namespace Ocelot.Errors.Middleware { using Configuration; - using System; - using System.Linq; - using System.Threading.Tasks; using Ocelot.Configuration.Repository; using Ocelot.Infrastructure.Extensions; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; - + using System; + using System.Linq; + using System.Threading.Tasks; + /// /// Catches all unhandled exceptions thrown by middleware, logs and returns a 500 /// @@ -21,7 +21,7 @@ namespace Ocelot.Errors.Middleware public ExceptionHandlerMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, - IInternalConfigurationRepository configRepo, + IInternalConfigurationRepository configRepo, IRequestScopedDataRepository repo) : base(loggerFactory.CreateLogger()) { @@ -34,6 +34,8 @@ namespace Ocelot.Errors.Middleware { try { + context.HttpContext.RequestAborted.ThrowIfCancellationRequested(); + //try and get the global request id and set it for logs... //should this basically be immutable per request...i guess it should! //first thing is get config @@ -52,6 +54,14 @@ namespace Ocelot.Errors.Middleware await _next.Invoke(context); } + catch (OperationCanceledException e) when (context.HttpContext.RequestAborted.IsCancellationRequested) + { + Logger.LogDebug("operation canceled"); + if (!context.HttpContext.Response.HasStarted) + { + context.HttpContext.Response.StatusCode = 499; + } + } catch (Exception e) { Logger.LogDebug("error calling middleware"); @@ -59,7 +69,7 @@ namespace Ocelot.Errors.Middleware var message = CreateMessage(context, e); Logger.LogError(message, e); - + SetInternalServerErrorOnResponse(context); } diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index dd2c249a..d8b6f333 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -2,42 +2,43 @@ { public enum OcelotErrorCode { - UnauthenticatedError, - UnknownError, - DownstreampathTemplateAlreadyUsedError, - UnableToFindDownstreamRouteError, - CannotAddDataError, - CannotFindDataError, - UnableToCompleteRequestError, - UnableToCreateAuthenticationHandlerError, - UnsupportedAuthenticationProviderError, - CannotFindClaimError, - ParsingConfigurationHeaderError, - NoInstructionsError, - InstructionNotForClaimsError, - UnauthorizedError, - ClaimValueNotAuthorisedError, - ScopeNotAuthorisedError, - UserDoesNotHaveClaimError, - DownstreamPathTemplateContainsSchemeError, - DownstreamPathNullOrEmptyError, - DownstreamSchemeNullOrEmptyError, - DownstreamHostNullOrEmptyError, - ServicesAreNullError, - ServicesAreEmptyError, - UnableToFindServiceDiscoveryProviderError, - UnableToFindLoadBalancerError, - RequestTimedOutError, - UnableToFindQoSProviderError, - UnmappableRequestError, - RateLimitOptionsError, - PathTemplateDoesntStartWithForwardSlash, - FileValidationFailedError, - UnableToFindDelegatingHandlerProviderError, - CouldNotFindPlaceholderError, - CouldNotFindAggregatorError, - CannotAddPlaceholderError, - CannotRemovePlaceholderError, - QuotaExceededError + UnauthenticatedError = 0, + UnknownError = 1, + DownstreampathTemplateAlreadyUsedError = 2, + UnableToFindDownstreamRouteError = 3, + CannotAddDataError = 4, + CannotFindDataError = 5, + UnableToCompleteRequestError = 6, + UnableToCreateAuthenticationHandlerError = 7, + UnsupportedAuthenticationProviderError = 8, + CannotFindClaimError = 9, + ParsingConfigurationHeaderError = 10, + NoInstructionsError = 11, + InstructionNotForClaimsError = 12, + UnauthorizedError = 13, + ClaimValueNotAuthorisedError = 14, + ScopeNotAuthorisedError = 15, + UserDoesNotHaveClaimError = 16, + DownstreamPathTemplateContainsSchemeError = 17, + DownstreamPathNullOrEmptyError = 18, + DownstreamSchemeNullOrEmptyError = 19, + DownstreamHostNullOrEmptyError = 20, + ServicesAreNullError = 21, + ServicesAreEmptyError = 22, + UnableToFindServiceDiscoveryProviderError = 23, + UnableToFindLoadBalancerError = 24, + RequestTimedOutError = 25, + UnableToFindQoSProviderError = 26, + UnmappableRequestError = 27, + RateLimitOptionsError = 28, + PathTemplateDoesntStartWithForwardSlash = 29, + FileValidationFailedError = 30, + UnableToFindDelegatingHandlerProviderError = 31, + CouldNotFindPlaceholderError = 32, + CouldNotFindAggregatorError = 33, + CannotAddPlaceholderError = 34, + CannotRemovePlaceholderError = 35, + QuotaExceededError = 36, + RequestCanceled = 37, } -} +} diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index 0705e7d2..eecf37db 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -1,9 +1,9 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Responses; +using System; +using System.Net.Http; +using System.Threading.Tasks; namespace Ocelot.Requester { @@ -16,7 +16,7 @@ namespace Ocelot.Requester public HttpClientHttpRequester(IOcelotLoggerFactory loggerFactory, IHttpClientCache cacheHandlers, - IDelegatingHandlerHandlerFactory factory, + IDelegatingHandlerHandlerFactory factory, IExceptionToErrorMapper mapper) { _logger = loggerFactory.CreateLogger(); @@ -33,7 +33,7 @@ namespace Ocelot.Requester try { - var response = await httpClient.SendAsync(context.DownstreamRequest.ToHttpRequestMessage()); + var response = await httpClient.SendAsync(context.DownstreamRequest.ToHttpRequestMessage(), context.HttpContext.RequestAborted); return new OkResponse(response); } catch (Exception exception) diff --git a/src/Ocelot/Requester/HttpClientWrapper.cs b/src/Ocelot/Requester/HttpClientWrapper.cs index b1f0345a..3f20bb37 100644 --- a/src/Ocelot/Requester/HttpClientWrapper.cs +++ b/src/Ocelot/Requester/HttpClientWrapper.cs @@ -1,4 +1,5 @@ using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace Ocelot.Requester @@ -15,9 +16,9 @@ namespace Ocelot.Requester Client = client; } - public Task SendAsync(HttpRequestMessage request) + public Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - return Client.SendAsync(request); + return Client.SendAsync(request, cancellationToken); } } } diff --git a/src/Ocelot/Requester/HttpExeptionToErrorMapper.cs b/src/Ocelot/Requester/HttpExeptionToErrorMapper.cs index 27e9c4ba..100d01f4 100644 --- a/src/Ocelot/Requester/HttpExeptionToErrorMapper.cs +++ b/src/Ocelot/Requester/HttpExeptionToErrorMapper.cs @@ -1,9 +1,9 @@ namespace Ocelot.Requester { - using System; - using System.Collections.Generic; using Errors; using Microsoft.Extensions.DependencyInjection; + using System; + using System.Collections.Generic; public class HttpExeptionToErrorMapper : IExceptionToErrorMapper { @@ -23,6 +23,11 @@ namespace Ocelot.Requester return _mappers[type](exception); } + if (type == typeof(OperationCanceledException)) + { + return new RequestCanceledError(exception.Message); + } + return new UnableToCompleteRequestError(exception); } } diff --git a/src/Ocelot/Requester/IHttpClient.cs b/src/Ocelot/Requester/IHttpClient.cs index 1912ab0b..9cca2198 100644 --- a/src/Ocelot/Requester/IHttpClient.cs +++ b/src/Ocelot/Requester/IHttpClient.cs @@ -1,5 +1,5 @@ -using System; -using System.Net.Http; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace Ocelot.Requester @@ -8,6 +8,6 @@ namespace Ocelot.Requester { HttpClient Client { get; } - Task SendAsync(HttpRequestMessage request); + Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); } -} +} diff --git a/src/Ocelot/Requester/RequestCanceledError.cs b/src/Ocelot/Requester/RequestCanceledError.cs new file mode 100644 index 00000000..1944fada --- /dev/null +++ b/src/Ocelot/Requester/RequestCanceledError.cs @@ -0,0 +1,11 @@ +using Ocelot.Errors; + +namespace Ocelot.Requester +{ + public class RequestCanceledError : Error + { + public RequestCanceledError(string message) : base(message, OcelotErrorCode.RequestCanceled) + { + } + } +} diff --git a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs index a417f2f9..c50daa27 100644 --- a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs +++ b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs @@ -1,6 +1,6 @@ +using Ocelot.Errors; using System.Collections.Generic; using System.Linq; -using Ocelot.Errors; namespace Ocelot.Responder { @@ -13,7 +13,7 @@ namespace Ocelot.Responder return 401; } - if (errors.Any(e => e.Code == OcelotErrorCode.UnauthorizedError + if (errors.Any(e => e.Code == OcelotErrorCode.UnauthorizedError || e.Code == OcelotErrorCode.ClaimValueNotAuthorisedError || e.Code == OcelotErrorCode.ScopeNotAuthorisedError || e.Code == OcelotErrorCode.UserDoesNotHaveClaimError @@ -27,6 +27,14 @@ namespace Ocelot.Responder return 503; } + if (errors.Any(e => e.Code == OcelotErrorCode.RequestCanceled)) + { + // status code refer to + // https://stackoverflow.com/questions/46234679/what-is-the-correct-http-status-code-for-a-cancelled-request?answertab=votes#tab-top + // https://httpstatuses.com/499 + return 499; + } + if (errors.Any(e => e.Code == OcelotErrorCode.UnableToFindDownstreamRouteError)) { return 404; @@ -40,4 +48,4 @@ namespace Ocelot.Responder return 404; } } -} +} diff --git a/test/Ocelot.UnitTests/Requester/HttpExeptionToErrorMapperTests.cs b/test/Ocelot.UnitTests/Requester/HttpExeptionToErrorMapperTests.cs index 04fb3a5e..9f6358ea 100644 --- a/test/Ocelot.UnitTests/Requester/HttpExeptionToErrorMapperTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpExeptionToErrorMapperTests.cs @@ -30,6 +30,14 @@ error.ShouldBeOfType(); } + [Fact] + public void should_return_request_canceled() + { + var error = _mapper.Map(new OperationCanceledException()); + + error.ShouldBeOfType(); + } + [Fact] public void should_return_error_from_mapper() { diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index 67acc123..8b550a05 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Net; -using Ocelot.Errors; +using Ocelot.Errors; using Ocelot.Responder; using Shouldly; +using System; +using System.Collections.Generic; +using System.Net; using TestStack.BDDfy; using Xunit; @@ -32,7 +32,7 @@ namespace Ocelot.UnitTests.Responder [InlineData(OcelotErrorCode.ClaimValueNotAuthorisedError)] [InlineData(OcelotErrorCode.ScopeNotAuthorisedError)] [InlineData(OcelotErrorCode.UnauthorizedError)] - [InlineData(OcelotErrorCode.UserDoesNotHaveClaimError)] + [InlineData(OcelotErrorCode.UserDoesNotHaveClaimError)] public void should_return_forbidden(OcelotErrorCode errorCode) { ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.Forbidden); @@ -125,7 +125,7 @@ namespace Ocelot.UnitTests.Responder // If this test fails then it's because the number of error codes has changed. // You should make the appropriate changes to the test cases here to ensure // they cover all the error codes, and then modify this assertion. - Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(37, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); + Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(38, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); } private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode) @@ -137,7 +137,7 @@ namespace Ocelot.UnitTests.Responder { var errors = new List(); - foreach(var errorCode in errorCodes) + foreach (var errorCode in errorCodes) { errors.Add(new AnyError(errorCode)); } @@ -168,4 +168,4 @@ namespace Ocelot.UnitTests.Responder _result.ShouldBe((int)expectedCode); } } -} +}