PAYATHON 2026

Authorize.Net controller.Execute() 的 HttpUtility 初始化异常

排障赵师傅

结论

System.TypeInitializationException 表示 AuthorizeNet.Util.HttpUtility 的静态初始化代码执行失败。controller.Execute() 是首次触发该类型加载的位置。这通常不是 Authorize.Net 返回的支付错误,也和 TLS 1.2 没有直接关系。

只看最外层异常无法判断具体原因,需要继续检查 InnerException。在 .NET 8 项目中,常见原因有:

  • SDK 所需的配置项缺失或格式错误。也可能是 SDK 仍从旧式 App.config 读取配置,而项目只配置了 appsettings.json。
  • System.Configuration.ConfigurationManager 等依赖未正确还原,或者依赖版本发生冲突。
  • SDK 的目标框架或传递依赖与当前运行环境不兼容。
  • 发布裁剪、单文件发布或部署目录缺少依赖,造成反射或配置读取失败。

如果问题出在静态构造函数的配置解析或程序集加载过程,降级 SDK、清理项目和强制启用 TLS 都不会解决它。

先取得真正的异常

不要只记录 ex.Message。TypeInitializationException 会将原始异常放在 InnerException 中,而完整的 ex.ToString() 通常已经包含整条异常链。

try
{
    controller.Execute();
}
catch (TypeInitializationException ex)
{
    Console.Error.WriteLine(ex.ToString());

    Exception? current = ex.InnerException;
    var level = 1;

    while (current != null)
    {
        Console.Error.WriteLine(
            $"InnerException #{level}: {current.GetType().FullName}: {current.Message}");

        current = current.InnerException;
        level++;
    }

    throw;
}

排查时主要看最内层异常的类型和消息:

  • FileNotFoundException、FileLoadException:依赖程序集缺失或版本不匹配。
  • ConfigurationErrorsException:配置文件、配置节或配置值有问题。
  • FormatException:某个字符串配置无法转换成数字或布尔值。
  • NullReferenceException:SDK 没有妥善处理缺失的配置项。
  • TypeLoadException、MissingMethodException:SDK 与实际加载的依赖版本不兼容。

完整堆栈也要保留。如果堆栈中出现具体的配置键、ConfigurationManager.AppSettings 或程序集名称,就可以据此缩小排查范围。

检查 NuGet 依赖

先确认项目最终解析了哪些包,包括传递依赖:

dotnet list package --include-transitive
dotnet restore --force-evaluate
dotnet build

检查以下内容:

  • 项目中是否同时存在多个 AuthorizeNet 版本。
  • System.Configuration.ConfigurationManager 是否正常解析。
  • 构建过程中是否出现包降级、程序集冲突或目标框架不兼容警告。
  • 部署目录中的 DLL 是否与本地编译时使用的版本一致。

如果最内层异常明确指出缺少 System.Configuration.ConfigurationManager,可以添加显式引用。具体版本应与项目当前的 .NET 8 依赖解析结果一致,不要在没有检查异常和依赖树的情况下随意指定。例如:

<ItemGroup>
  <PackageReference Include="AuthorizeNet" Version="2.0.4" />
  <PackageReference Include="System.Configuration.ConfigurationManager"
                    Version="8.0.0" />
</ItemGroup>

这里的 8.0.0 只是 .NET 8 项目中的常见选择,并不适用于所有 AuthorizeNet 依赖组合。最终版本要根据 NuGet 解析结果和最内层异常确定。

区分 App.config 和 appsettings.json

旧式 SDK 可能通过以下 API 读取配置:

System.Configuration.ConfigurationManager.AppSettings["someKey"]

这个 API 不会自动读取 ASP.NET Core 的 appsettings.json:

{
  "SomeKey": "value"
}

如果异常堆栈表明 HttpUtility 正在读取某个 appSettings 键,就要按该 SDK 实际使用的键名配置 App.config,或者升级到不依赖旧式配置读取方式的 SDK 版本。配置结构可以参考:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="httpConnectionTimeout" value="30000" />
  </appSettings>
</configuration>

httpConnectionTimeout 只是一个可能出现的配置示例。只有异常信息或对应版本的 SDK 源码确认读取了这个键,才需要添加。不要根据猜测不断增加配置项。

还要检查输出目录中是否生成了对应的 .dll.config 文件,以及文件里的值是否仍然有效。数字配置要使用纯整数,不能写成 "30s"、空字符串或带单位的值。

修正示例代码中的认证参数

当前代码还存在一个独立问题。方法接收的 ApiLoginID 和 ApiTransactionKey 被写入 ApiOperationBase,实际请求却使用了两个空的局部变量:

var apiLoginId = "";
var transactionKey = "";

后续认证对象也使用了这两个变量:

var merchantAuth = new merchantAuthenticationType
{
    name = apiLoginId,
    ItemElementName = ItemChoiceType.transactionKey,
    Item = transactionKey
};

即使解决了 HttpUtility 初始化异常,这段代码仍会用空凭据发送请求。Authorize.Net 通常会返回认证失败,而不是 TypeInitializationException。

可以只保留一套认证信息:

public static ANetApiResponse Run(
    string apiLoginId,
    string transactionKey,
    decimal amount)
{
    ApiOperationBase<ANetApiRequest, ANetApiResponse>.RunEnvironment =
        AuthorizeNet.Environment.SANDBOX;

    var merchantAuth = new merchantAuthenticationType
    {
        name = apiLoginId,
        ItemElementName = ItemChoiceType.transactionKey,
        Item = transactionKey
    };

    var transactionRequest = new transactionRequestType
    {
        transactionType =
            transactionTypeEnum.authCaptureTransaction.ToString(),
        amount = amount
    };

    var request = new getHostedPaymentPageRequest
    {
        merchantAuthentication = merchantAuth,
        transactionRequest = transactionRequest,
        hostedPaymentSettings =
        [
            new settingType
            {
                settingName = "hostedPaymentReturnOptions",
                settingValue =
                    """
                    {
                      "showReceipt": false,
                      "url": "https://yourwebsite.com/payment-success",
                      "urlText": "Continue"
                    }
                    """
            }
        ]
    };

    var controller = new getHostedPaymentPageController(request);

    try
    {
        controller.Execute();
    }
    catch (TypeInitializationException ex)
    {
        Console.Error.WriteLine(ex.ToString());
        throw;
    }

    return controller.GetApiResponse();
}

hostedPaymentSettings 支持的名称和 JSON 结构,应以当前 Authorize.Net Accept Hosted 接口及项目使用的 SDK 版本为准。如果初始化问题解决后出现接口校验错误,再单独检查这些设置。

原代码中还有几处需要删除或修正:

  • RunEnvironment 和 MerchantAuthentication 被重复赋值。
  • amount 参数没有使用,交易金额被固定为 50.00m。
  • creditCard、billingAddress、paymentType 和 lineItems 创建后没有加入请求。
  • 请求使用的是局部空凭据,与 ApiOperationBase 中设置的凭据不一致。

这些问题不会导致 HttpUtility 静态初始化失败,但会影响后续请求。

如果只在发布后出现

如果调试运行正常,发布后才报错,可以暂时关闭裁剪和单文件发布进行对比:

<PropertyGroup>
  <PublishTrimmed>false</PublishTrimmed>
  <PublishSingleFile>false</PublishSingleFile>
</PropertyGroup>

旧式 SDK 可能依赖反射、配置文件或运行时动态加载,相关类型或程序集可能在裁剪时被移除。如果关闭这些选项后异常消失,就需要保留对应程序集,或者不要对该应用启用裁剪。

同时确认部署环境的目标架构和 .NET 8 Runtime 是否正确,并检查发布目录中有没有异常所指向的依赖文件。

TLS 设置为什么没有效果

ServicePointManager.SecurityProtocol 控制网络连接使用的协议,而静态类型初始化通常发生在 HTTP 请求真正发出之前。如果 HttpUtility 在读取配置或加载程序集时就失败了,程序尚未进入 TLS 握手阶段,下面的设置自然不会改变结果:

ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

在现代 .NET 中,HTTP 栈通常会根据操作系统和运行时配置协商 TLS。只有最内层异常明确指向连接、证书或 TLS 握手问题,才需要继续排查网络协议。

建议的排查顺序

  1. 输出完整的 TypeInitializationException.ToString() 和所有 InnerException。
  2. 根据最内层异常判断问题属于配置错误、程序集缺失、版本冲突还是发布裁剪。
  3. 使用 dotnet list package --include-transitive 检查项目实际解析的依赖。
  4. 如果 SDK 使用 ConfigurationManager.AppSettings,确认配置位于它可以读取的配置文件中。
  5. 清理重复配置,确保请求使用方法传入的真实 Sandbox 凭据。
  6. 在关闭裁剪和单文件发布的普通 Debug 环境中创建最小复现项目。
  7. 修复后重新启动进程。静态构造函数失败后,CLR 会在当前进程中继续将该类型视为初始化失败,重复调用 Execute() 通常无法恢复。

判断原因时应以最内层异常为准。缺少 InnerException 的具体内容,就无法确定问题来自哪个配置键或哪个依赖版本。

备注:内容仅供参考。