Merge pull request #16318 from abpframework/Ajax_MultiTenancyMiddlewareErrorPageBuilder

Return JSON results for ajax requests.
pull/16551/head
liangshiwei 2 years ago committed by GitHub
commit d610b40ddc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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", HtmlEncoder.Default.Encode(exception.Message));
if (isCookieAuthentication && context.Request.Method.Equals("Get", StringComparison.OrdinalIgnoreCase) && !context.Request.IsAjax())
{
context.Response.Headers.Add("Abp-Tenant-Resolve-Error", HtmlEncoder.Default.Encode(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<IOptions<JsonOptions>>().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", HtmlEncoder.Default.Encode(exception.Message));
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
context.Response.ContentType = "text/html";

@ -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;
/// <summary>
/// https://github.com/dotnet/aspnetcore/blob/release/7.0/src/Shared/ResponseContentTypeHelper.cs
/// </summary>
public static class ResponseContentTypeHelper
{
/// <summary>
/// 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. <see cref="ContentType"/> property set on <see cref="HttpResponse"/>
/// 3. Default content type set on the action result
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static void ResolveContentTypeAndEncoding(
string? actionResultContentType,
string? httpResponseContentType,
(string defaultContentType, Encoding defaultEncoding) @default,
Func<string, Encoding> 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;
}
}

@ -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}=<script>alert(hi)</script>", HttpStatusCode.NotFound);
result.ShouldNotContain("<script>alert(hi)</script>");
}
[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<RemoteServiceErrorResponse>(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");
}
}
}

Loading…
Cancel
Save