From 3d9451fd66105c140d9926579d4a1a235e2862c2 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 19 Apr 2023 19:53:45 +0800 Subject: [PATCH] Return JSON results for ajax requests. --- .../AbpAspNetCoreMultiTenancyOptions.cs | 69 +++++++++++++++- .../Internal/ResponseContentTypeHelper.cs | 78 +++++++++++++++++++ ...TenancyMiddlewareErrorPageBuilder_Tests.cs | 18 ++++- 3 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Internal/ResponseContentTypeHelper.cs diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs index 5956e4f7d3..b63518cc8e 100644 --- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs @@ -1,14 +1,23 @@ using System; using System.Globalization; using System.Net; +using System.Runtime.ExceptionServices; +using System.Text; using System.Text.Encodings.Web; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Volo.Abp.Http; +using Volo.Abp.Json; using Volo.Abp.MultiTenancy; namespace Volo.Abp.AspNetCore.MultiTenancy; @@ -58,14 +67,70 @@ public class AbpAspNetCoreMultiTenancyOptions } } + context.Response.Headers.Add("Abp-Tenant-Resolve-Error", exception.Message); if (isCookieAuthentication && context.Request.Method.Equals("Get", StringComparison.OrdinalIgnoreCase) && !context.Request.IsAjax()) { - context.Response.Headers.Add("Abp-Tenant-Resolve-Error", exception.Message); context.Response.Redirect(context.Request.GetEncodedUrl()); } + else if (context.Request.IsAjax()) + { + var error = new RemoteServiceErrorResponse(new RemoteServiceErrorInfo(exception.Message, exception is BusinessException businessException ? businessException.Details : string.Empty)); + + var jsonSerializerOptions = context.RequestServices.GetRequiredService>().Value.SerializerOptions; + + ResponseContentTypeHelper.ResolveContentTypeAndEncoding( + null, + context.Response.ContentType, + (new MediaTypeHeaderValue("application/json") + { + Encoding = Encoding.UTF8 + }.ToString(), Encoding.UTF8), + MediaType.GetEncoding, + out var resolvedContentType, + out var resolvedContentTypeEncoding); + + context.Response.ContentType = resolvedContentType; + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + + var responseStream = context.Response.Body; + if (resolvedContentTypeEncoding.CodePage == Encoding.UTF8.CodePage) + { + try + { + await JsonSerializer.SerializeAsync(responseStream, error, error.GetType(), jsonSerializerOptions, context.RequestAborted); + await responseStream.FlushAsync(context.RequestAborted); + } + catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) { } + } + else + { + var transcodingStream = Encoding.CreateTranscodingStream(context.Response.Body, resolvedContentTypeEncoding, Encoding.UTF8, leaveOpen: true); + ExceptionDispatchInfo exceptionDispatchInfo = null; + try + { + await JsonSerializer.SerializeAsync(transcodingStream, error, error.GetType(), jsonSerializerOptions, context.RequestAborted); + await transcodingStream.FlushAsync(context.RequestAborted); + } + catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) { } + catch (Exception ex) + { + exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex); + } + finally + { + try + { + await transcodingStream.DisposeAsync(); + } + catch when (exceptionDispatchInfo != null) + { + } + exceptionDispatchInfo?.Throw(); + } + } + } else { - context.Response.Headers.Add("Abp-Tenant-Resolve-Error", exception.Message); context.Response.StatusCode = (int)HttpStatusCode.NotFound; context.Response.ContentType = "text/html"; diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Internal/ResponseContentTypeHelper.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Internal/ResponseContentTypeHelper.cs new file mode 100644 index 0000000000..bff1290835 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Internal/ResponseContentTypeHelper.cs @@ -0,0 +1,78 @@ +using System; +using System.Net.Mime; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Internal; + +/// +/// https://github.com/dotnet/aspnetcore/blob/release/7.0/src/Shared/ResponseContentTypeHelper.cs +/// +public static class ResponseContentTypeHelper +{ + /// + /// Gets the content type and encoding that need to be used for the response. + /// The priority for selecting the content type is: + /// 1. ContentType property set on the action result + /// 2. property set on + /// 3. Default content type set on the action result + /// + /// + /// The user supplied content type is not modified and is used as is. For example, if user + /// sets the content type to be "text/plain" without any encoding, then the default content type's + /// encoding is used to write the response and the ContentType header is set to be "text/plain" without any + /// "charset" information. + /// + public static void ResolveContentTypeAndEncoding( + string? actionResultContentType, + string? httpResponseContentType, + (string defaultContentType, Encoding defaultEncoding) @default, + Func getEncoding, + out string resolvedContentType, + out Encoding resolvedContentTypeEncoding) + { + var (defaultContentType, defaultContentTypeEncoding) = @default; + + // 1. User sets the ContentType property on the action result + if (actionResultContentType != null) + { + resolvedContentType = actionResultContentType; + var actionResultEncoding = getEncoding(actionResultContentType); + resolvedContentTypeEncoding = actionResultEncoding ?? defaultContentTypeEncoding; + return; + } + + // 2. User sets the ContentType property on the http response directly + if (!string.IsNullOrEmpty(httpResponseContentType)) + { + var mediaTypeEncoding = getEncoding(httpResponseContentType); + if (mediaTypeEncoding != null) + { + resolvedContentType = httpResponseContentType; + resolvedContentTypeEncoding = mediaTypeEncoding; + } + else + { + resolvedContentType = httpResponseContentType; + resolvedContentTypeEncoding = defaultContentTypeEncoding; + } + + return; + } + + // 3. Fall-back to the default content type + resolvedContentType = defaultContentType; + resolvedContentTypeEncoding = defaultContentTypeEncoding; + } + + public static Encoding GetEncoding(string mediaType) + { + if (MediaTypeHeaderValue.TryParse(mediaType, out var parsed)) + { + return parsed.Encoding; + } + + return default; + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_MultiTenancyMiddlewareErrorPageBuilder_Tests.cs b/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_MultiTenancyMiddlewareErrorPageBuilder_Tests.cs index 8932745f29..2d2de48992 100644 --- a/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_MultiTenancyMiddlewareErrorPageBuilder_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_MultiTenancyMiddlewareErrorPageBuilder_Tests.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; -using System.Net; +using System.Net; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Shouldly; +using Volo.Abp.Http; using Xunit; namespace Volo.Abp.AspNetCore.MultiTenancy; @@ -23,4 +24,17 @@ public class AspNetCoreMultiTenancy_MultiTenancyMiddlewareErrorPageBuilder_Tests var result = await GetResponseAsStringAsync($"http://abp.io?{_options.TenantKey}=", HttpStatusCode.NotFound); result.ShouldNotContain(""); } + + [Fact] + public async Task MultiTenancyMiddlewareErrorPageBuilder_Ajax_Test() + { + using (var response = await GetResponseAsync($"http://abp.io?{_options.TenantKey}=abpio", HttpStatusCode.NotFound, xmlHttpRequest: true)) + { + var result = await response.Content.ReadAsStringAsync(); + var error = JsonSerializer.Deserialize(result, new JsonSerializerOptions {PropertyNamingPolicy = JsonNamingPolicy.CamelCase}); + error.Error.ShouldNotBeNull(); + error.Error.Message.ShouldBe("Tenant not found!"); + error.Error.Details.ShouldBe("There is no tenant with the tenant id or name: abpio"); + } + } }