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
- Make streamFactory always return a valid writable Stream (e.g. File.Create / new MemoryStream).
- If an item should be skipped, filter download_bill_list items before downloading rather than returning null.
- 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
- Never return null from a stream factory; throw a meaningful exception instead
- Test the factory with real items before wiring it in
- Prefer File.Create or MemoryStream as safe defaults
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
- streamFactory 返回的流必须可写。
- AppId 不能为空。
- AppId 不能为空。
- 业务申请编号和微信支付申请单编号至少填写一个。
- ArgumentNullException (destination is null)
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)