JeffreySu/WeiXinMPSDK · error · InvalidOperationException

streamFactory 不能返回 null。

Error message

streamFactory 不能返回 null。

What it means

When downloading merchant fund bills, the library calls the caller-provided streamFactory delegate to obtain a writable Stream for each bill file. If the delegate returns null, an InvalidOperationException is thrown because the download cannot proceed without a destination stream.

Solutions

  1. Make streamFactory always return a valid writable Stream (e.g. File.Create / new MemoryStream).
  2. If an item should be skipped, filter download_bill_list items before downloading rather than returning null.
  3. Check the factory for early-return paths that yield null and replace with stream creation or an explicit exception.

Example fix

// before
Stream streamFactory(SubmerchantBillDownloadItem item) => cache[item.download_url]; // may be null
// after
Stream streamFactory(SubmerchantBillDownloadItem item) =>
    cache.TryGetValue(item.download_url, out var s) && s.CanWrite ? s : File.Create(tempPath(item));
Defensive patterns

Strategy: validation

Validate before calling

var stream = streamFactory(item);
if (stream == null) throw new InvalidOperationException("streamFactory must return a writable stream.");

Type guard

bool factoryValid = streamFactory(item) is Stream;

Try / catch

try { await apis.DownloadFundFlowBillAsync(..., streamFactory); }
catch (InvalidOperationException ex) when (ex.Message.Contains("streamFactory")) { logger.Error(ex, "streamFactory returned null"); throw; }

Prevention

When it happens

Trigger: Calling a fund-bill download API (DownloadSubmerchantFundFlowBill style) with a streamFactory lambda that can return null, e.g. returning null for certain items or a factory built on File.OpenRead-style logic that fails silently.

Common situations: Factories that look up an open stream from a cache/dictionary and miss; returning null for filtered items; reusing a disposed stream abstraction that reports null.

Related errors


AI-assisted analysis of JeffreySu/WeiXinMPSDK@be573f6f94 (2026-09-12). Data as JSON: /api/errors/4777e7dbcab6e487. Report an issue: GitHub.

Appendix: source

Thrown at src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/Apis/BasePay/BasePayApis.cs:918

        {
            if (streamFactory == null)
            {
                throw new ArgumentNullException(nameof(streamFactory));
            }

            try
            {
                var url = BasePayApis.GetPayApiUrl($"{Senparc.Weixin.Config.TenPayV3Host}/{{0}}v3/bill/sub-merchant-fundflowbill{UrlQueryHelper.ToParams(data)}");
                TenPayApiRequest tenPayApiRequest = new(_tenpayV3Setting);
                var result = await tenPayApiRequest.RequestAsync<SubmerchantBillReturnJson>(url, null, cancellationToken, timeOut, ApiRequestMethod.GET).ConfigureAwait(false);

                //下载资金账单
                if (result.VerifySignSuccess == true)
                {
                    foreach (var item in result.download_bill_list ?? new List<SubmerchantBillDownloadItem>())
                    {
                        cancellationToken.ThrowIfCancellationRequested();
                        var fileStream = streamFactory(item) ?? throw new InvalidOperationException("streamFactory 不能返回 null。");
                        if (!fileStream.CanWrite)
                        {
                            throw new ArgumentException("streamFactory 返回的流必须可写。", nameof(streamFactory));
                        }
                        var fileVerify = await TenPayDownloadHelper.DownloadAndVerifyAsync(
                            tenPayApiRequest, item.download_url, fileStream, item.hash_type, item.hash_value,
                            timeOut, cancellationToken).ConfigureAwait(false);
                        if (!fileVerify)
                        {
                            result.VerifySignSuccess = false;
                            result.ResultCode.Additional += "请求成功,但文件校验错误。请查看日志!";
                            SenparcTrace.BaseExceptionLog(new TenpayApiRequestException($"SubmerchantFundflowBillQueryAsync 下载文件成功,但流式校验失败,正确值:{item.hash_value}"));
                        }
                    }
                }

                return result;
            }

View on GitHub (pinned to be573f6f94)