diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/ActionResults/OperationOutcomeResult.cs b/src/Microsoft.Health.Fhir.Api/Features/ActionResults/OperationOutcomeResult.cs similarity index 100% rename from src/Microsoft.Health.Fhir.Shared.Api/Features/ActionResults/OperationOutcomeResult.cs rename to src/Microsoft.Health.Fhir.Api/Features/ActionResults/OperationOutcomeResult.cs diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs b/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs index e639fa1ee4..77033da784 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs @@ -3,6 +3,7 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------------------------------------------- +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -10,8 +11,12 @@ using System.Threading.Tasks; using System.Web; using EnsureThat; +using Hl7.Fhir.Model; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; +using Microsoft.Health.Fhir.Api.Features.ActionResults; +using Microsoft.Health.Fhir.Core.Extensions; using Microsoft.Health.Fhir.Core.Features.Routing; namespace Microsoft.Health.Fhir.Api.Features.Routing @@ -19,42 +24,75 @@ namespace Microsoft.Health.Fhir.Api.Features.Routing public class SearchPostReroutingMiddleware { private readonly RequestDelegate _next; + private readonly ILogger _logger; - public SearchPostReroutingMiddleware(RequestDelegate next) + public SearchPostReroutingMiddleware(RequestDelegate next, ILogger logger) { EnsureArg.IsNotNull(next); _next = next; + _logger = logger; } public async Task Invoke(HttpContext context) { var request = context.Request; - if (request != null - && request.Method == "POST" - && request.Path.Value.EndsWith(KnownRoutes.Search, System.StringComparison.OrdinalIgnoreCase)) + try { - if (request.ContentType is null || request.HasFormContentType) + if (request != null + && request.Method == "POST" + && request.Path.Value.EndsWith(KnownRoutes.Search, System.StringComparison.OrdinalIgnoreCase)) { - if (request.HasFormContentType) + if (request.ContentType is null || request.HasFormContentType) { - var mergedPairs = GetUniqueFormAndQueryStringKeyValues(HttpUtility.ParseQueryString(request.QueryString.ToString()), request.Form); - request.Query = mergedPairs; + _logger.LogInformation("Rerouting POST to GET with query parameters from form body."); + + if (request.HasFormContentType) + { + var mergedPairs = GetUniqueFormAndQueryStringKeyValues(HttpUtility.ParseQueryString(request.QueryString.ToString()), request.Form); + request.Query = mergedPairs; + } + + request.ContentType = null; + request.Form = null; + request.Path = request.Path.Value.Substring(0, request.Path.Value.Length - KnownRoutes.Search.Length); + request.Method = "GET"; } + else + { + _logger.LogDebug("Rejecting POST with invalid Content-Type."); - request.ContentType = null; - request.Form = null; - request.Path = request.Path.Value.Substring(0, request.Path.Value.Length - KnownRoutes.Search.Length); - request.Method = "GET"; - } - else - { - context.Response.Clear(); - context.Response.StatusCode = (int)HttpStatusCode.BadRequest; - await context.Response.WriteAsync(Api.Resources.ContentTypeFormUrlEncodedExpected); - return; + context.Response.Clear(); + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + + var operationOutcome = new OperationOutcome + { + Id = Guid.NewGuid().ToString(), + Issue = new List() + { + new OperationOutcome.IssueComponent() + { + Severity = OperationOutcome.IssueSeverity.Error, + Code = OperationOutcome.IssueType.Invalid, + Diagnostics = Api.Resources.ContentTypeFormUrlEncodedExpected, + }, + }, + Meta = new Meta() + { + LastUpdated = Clock.UtcNow, + }, + }; + + await context.Response.WriteAsJsonAsync(operationOutcome); + return; + } } } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred while rerouting POST search to GET."); + throw; + } await _next.Invoke(context); } diff --git a/src/Microsoft.Health.Fhir.Azure.UnitTests/Api/SearchPostReroutingMiddlewareTests.cs b/src/Microsoft.Health.Fhir.Azure.UnitTests/Api/SearchPostReroutingMiddlewareTests.cs index 8cb10bc046..17dfae0cfb 100644 --- a/src/Microsoft.Health.Fhir.Azure.UnitTests/Api/SearchPostReroutingMiddlewareTests.cs +++ b/src/Microsoft.Health.Fhir.Azure.UnitTests/Api/SearchPostReroutingMiddlewareTests.cs @@ -8,6 +8,7 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Health.Fhir.Api; using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Tests.Common; @@ -30,7 +31,7 @@ public SearchPostReroutingMiddlewareTests() { _httpContext = new DefaultHttpContext(); _requestDelegate = Substitute.For(); - _middleware = new SearchPostReroutingMiddleware(_requestDelegate); + _middleware = new SearchPostReroutingMiddleware(_requestDelegate, new NullLogger()); } [Theory] @@ -64,7 +65,9 @@ public async Task GivenSearchRequestViaPost_WhenContentTypeIsSpecified_InvalidCo _httpContext.Response.Body.Position = 0; using var reader = new StreamReader(_httpContext.Response.Body); var body = reader.ReadToEnd(); - Assert.Equal(ApiResources.ContentTypeFormUrlEncodedExpected, body); + var expectedString = ApiResources.ContentTypeFormUrlEncodedExpected.Replace("\"", "\\\""); + expectedString = $"\"{expectedString}\""; + Assert.Contains(expectedString, body); Assert.Equal((int)HttpStatusCode.BadRequest, _httpContext.Response.StatusCode); } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Exceptions/BaseExceptionMiddleware.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Exceptions/BaseExceptionMiddleware.cs index 6d53abc136..aed4b83555 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Exceptions/BaseExceptionMiddleware.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Exceptions/BaseExceptionMiddleware.cs @@ -114,6 +114,8 @@ public async Task Invoke(HttpContext context) doesOperationOutcomeHaveError = true; + _logger.LogError(exception, "An unhandled exception occurred while processing the request"); + await ExecuteResultAsync(context, result); } finally diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems b/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems index 000d1f841a..3e4fcd81b9 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems @@ -28,7 +28,6 @@ - diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlRetry/SqlRetryService.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlRetry/SqlRetryService.cs index 6094e53677..701199d833 100644 --- a/src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlRetry/SqlRetryService.cs +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlRetry/SqlRetryService.cs @@ -422,19 +422,19 @@ public async Task GetConnection(ISqlConnectionBuilder sqlConnecti { SqlConnection conn; var sw = Stopwatch.StartNew(); - var logSB = new StringBuilder("Long running retrieve SQL connection"); + var logSB = new StringBuilder("Long running retrieve SQL connection. "); var isReadOnlyConnection = isReadOnly ? "read-only " : string.Empty; if (!isReadOnly || !_coreFeatureConfiguration.SupportsSqlReplicas) { - logSB.AppendLine("Not read only"); + logSB.AppendLine("Not read only. "); conn = await sqlConnectionBuilder.GetSqlConnectionAsync(false, applicationName); } else { - logSB.AppendLine("Checking read only"); + logSB.AppendLine("Checking read only. "); var replicaTrafficRatio = GetReplicaTrafficRatio(sqlConnectionBuilder, logger); - logSB.AppendLine($"Got replica traffic ratio in {sw.Elapsed.TotalSeconds} seconds. Ratio is {replicaTrafficRatio}"); + logSB.AppendLine($"Got replica traffic ratio in {sw.Elapsed.TotalSeconds} seconds. Ratio is {replicaTrafficRatio}. "); if (replicaTrafficRatio < 0.5) // it does not make sense to use replica less than master at all {