diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator.cs index 18af3e0661..ad44a98316 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator.cs @@ -136,9 +136,7 @@ public class JQueryProxyScriptGenerator : IProxyScriptGenerator, ITransientDepen AddAjaxCallParameters(script, action); var ajaxParamsIsFromForm = action.Parameters.Any(x => x.BindingSourceId == ParameterBindingSources.Form); - var dataType = action.ReturnValue.Type == ReturnValueApiDescriptionModel.Create(typeof(string)).Type - ? "{ dataType: 'text' }, " - : string.Empty; + var dataType = GetJQueryDataTypeAndAcceptOverride(action); script.AppendLine(ajaxParamsIsFromForm ? " }, $.extend(true, {}, " + dataType + "{ contentType: 'application/x-www-form-urlencoded; charset=UTF-8' }, ajaxParams)));" : " }, " + dataType + "ajaxParams));"); @@ -146,6 +144,46 @@ public class JQueryProxyScriptGenerator : IProxyScriptGenerator, ITransientDepen script.AppendLine(" };"); } + private static string GetJQueryDataTypeAndAcceptOverride(ActionApiDescriptionModel action) + { + var contentTypes = action.ReturnValue.ContentTypes; + var isStringReturn = action.ReturnValue.Type == ReturnValueApiDescriptionModel.Create(typeof(string)).Type; + + if (contentTypes is { Count: > 0 }) + { + var normalized = contentTypes.Select(NormalizeMediaType).ToList(); + + if (normalized.Any(IsJsonMediaType)) + { + return "{ dataType: 'json', headers: { Accept: 'application/json' } }, "; + } + + if (normalized.All(ct => ct.StartsWith("text/", StringComparison.OrdinalIgnoreCase))) + { + return "{ dataType: 'text', headers: { Accept: 'text/plain' } }, "; + } + } + + return isStringReturn ? "{ dataType: 'text' }, " : string.Empty; + } + + private static bool IsJsonMediaType(string normalizedMediaType) + { + return normalizedMediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase) || + normalizedMediaType.Equals("text/json", StringComparison.OrdinalIgnoreCase) || + normalizedMediaType.EndsWith("+json", StringComparison.OrdinalIgnoreCase); + } + + private static string NormalizeMediaType(string mediaType) + { + if (string.IsNullOrWhiteSpace(mediaType)) + { + return string.Empty; + } + var semi = mediaType.IndexOf(';'); + return (semi < 0 ? mediaType : mediaType.Substring(0, semi)).Trim(); + } + private static string FindBestApiVersion(ActionApiDescriptionModel action) { //var configuredVersion = GetConfiguredApiVersion(); //TODO: Implement diff --git a/framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator_ContentTypes_Tests.cs b/framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator_ContentTypes_Tests.cs new file mode 100644 index 0000000000..ee0a20114c --- /dev/null +++ b/framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator_ContentTypes_Tests.cs @@ -0,0 +1,129 @@ +#nullable enable +using System.Collections.Generic; +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.Http.Modeling; +using Volo.Abp.Http.ProxyScripting.Generators; +using Volo.Abp.Http.ProxyScripting.Generators.JQuery; +using Xunit; + +namespace Volo.Abp.Http.ProxyScripting.Generators.JQuery; + +public class JQueryProxyScriptGenerator_ContentTypes_Tests +{ + private readonly JQueryProxyScriptGenerator _generator = new( + Microsoft.Extensions.Options.Options.Create(new DynamicJavaScriptProxyOptions())); + + [Fact] + public void Should_Emit_Json_DataType_And_Accept_When_ContentTypes_Contain_Json() + { + var script = _generator.CreateScript(BuildAppModel( + returnType: "System.String", + contentTypes: new[] { "text/plain", "application/json", "text/json" })); + + script.ShouldContain("dataType: 'json'"); + script.ShouldContain("Accept: 'application/json'"); + script.ShouldNotContain("dataType: 'text'"); + } + + [Fact] + public void Should_Emit_Text_DataType_And_Accept_When_ContentTypes_Only_Text() + { + var script = _generator.CreateScript(BuildAppModel( + returnType: "System.String", + contentTypes: new[] { "text/plain", "text/csv" })); + + script.ShouldContain("dataType: 'text'"); + script.ShouldContain("Accept: 'text/plain'"); + } + + [Fact] + public void Should_Fallback_To_Legacy_Text_When_Return_Is_String_And_ContentTypes_Null() + { + var script = _generator.CreateScript(BuildAppModel( + returnType: "System.String", + contentTypes: null)); + + // Legacy behavior preserved: string return → dataType: 'text' (no Accept override) + script.ShouldContain("dataType: 'text'"); + script.ShouldNotContain("Accept:"); + } + + [Fact] + public void Should_Emit_No_DataType_For_Non_String_Return_Without_ContentTypes() + { + var script = _generator.CreateScript(BuildAppModel( + returnType: "System.Int32", + contentTypes: null)); + + script.ShouldNotContain("dataType: 'text'"); + script.ShouldNotContain("dataType: 'json'"); + } + + [Fact] + public void Should_Not_Emit_Json_DataType_When_Only_Binary_ContentTypes() + { + var script = _generator.CreateScript(BuildAppModel( + returnType: "System.Byte[]", + contentTypes: new[] { "application/octet-stream", "image/png" })); + + // jQuery dataType doesn't have a clean "blob" — fall through to no override + script.ShouldNotContain("dataType: 'json'"); + script.ShouldNotContain("dataType: 'text'"); + } + + [Fact] + public void Should_Prefer_Json_When_Json_Present_Even_With_Other_Types() + { + var script = _generator.CreateScript(BuildAppModel( + returnType: "My.Project.UserDto", + contentTypes: new[] { "application/xml", "application/json", "text/html" })); + + script.ShouldContain("dataType: 'json'"); + script.ShouldContain("Accept: 'application/json'"); + } + + [Fact] + public void Case_Insensitive_Json_Detection() + { + var script = _generator.CreateScript(BuildAppModel( + returnType: "System.String", + contentTypes: new[] { "APPLICATION/JSON" })); + + script.ShouldContain("dataType: 'json'"); + } + + private static ApplicationApiDescriptionModel BuildAppModel(string returnType, IList? contentTypes) + { + var model = ApplicationApiDescriptionModel.Create(); + var module = model.GetOrAddModule("app", "Default"); + var controller = module.GetOrAddController( + name: "TestController", + groupName: null, + isRemoteService: true, + isIntegrationService: false, + apiVersion: null, + type: typeof(object)); + + var action = new ActionApiDescriptionModel + { + UniqueName = "DoSomethingAsync", + Name = "DoSomethingAsync", + HttpMethod = "GET", + Url = "api/test/do-something", + SupportedVersions = new List(), + ParametersOnMethod = new List(), + Parameters = new List(), + ReturnValue = new ReturnValueApiDescriptionModel + { + Type = returnType, + TypeSimple = returnType, + ContentTypes = contentTypes, + }, + AuthorizeDatas = new List(), + }; + controller.AddAction("DoSomethingAsync", action); + + return model; + } +}