Versions: 2026.3.4 and 2026.4.1 (identical code, both affected)
Platform: Blazor Server (InteractiveServer), .NET 10.0.12, Linux (Docker)
Component: StiBlazorViewer
Summary
Stimulsoft.System.Web.HttpContext.Current is a single process-wide static object. StiBlazorComponent.GetRequestParams temporarily replaces its Request property, and StiRequestParamsHelper.Get then reads that property several times. With more than one viewer request in flight (different users' circuits, or the Task.Run'd async commands), another thread swaps Request in the middle of Get and Get throws a NullReferenceException.
Because StiBlazorViewer.Request is async void, the exception is unhandled and the user's Blazor circuit is terminated.
Production symptom
Code: Select all
System.NullReferenceException: Object reference not set to an instance of an object.
at Stimulsoft.Report.Web.StiRequestParamsHelper.Get(HttpContext httpContext)
at Stimulsoft.Report.Blazor.StiBlazorComponent.GetRequestParams(String url, String postData)
at Stimulsoft.Report.Blazor.StiBlazorViewer.Request(String requestId, String url, String data)
at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__124_0(Object state)
at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.PostAsync[TState](Task antecedent, Action`1 callback, TState state)Root cause (from decompiling the shipped assemblies)
StiBlazorComponent.GetRequestParams (static):
Code: Select all
HttpRequest request = HttpContext.Current.Request;
HttpContext.Current.Request = new HttpRequest(url, postData);
StiRequestParams p = StiRequestParamsHelper.Get(HttpContext.Current);
HttpContext.Current.Request = request;Code: Select all
public static HttpContext Current { get; } = new HttpContext(); // one instance per process
public HttpRequest Request { get; set; }Code: Select all
foreach (string key in httpContext.Request.Params.Keys)
if (key != null && key.StartsWith("stiweb_"))
stiRequestParams.All[key] = httpContext.Request.Params[key].ToString();The same shared state also causes:
- The save/restore pattern is itself racy: thread B can save thread A's temporary request as "the original", so Current.Request is left pointing at a stale request.
- HttpRequest.Params and HttpRequest.Form are lazily initialised without synchronisation, and NameValueCollection and Hashtable are not thread-safe. Under contention this produced IndexOutOfRangeException, ArgumentException from Hashtable.Insert and InvalidOperationException from NameObjectKeysEnumerator.MoveNext.
- CreateDefaultRequestParams() also assigns HttpContext.Current.Request (and never restores it), so it can race with GetRequestParams as well.
Console app, net10.0, PackageReference Stimulsoft.Reports.Blazor 2026.4.1.
Code: Select all
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Stimulsoft.Report.Blazor;
var errors = new ConcurrentDictionary<string, int>();
var ok = 0;
var post = "stiweb_component=Viewer&stiweb_action=GetPages&stiweb_theme=Office2022" +
"&stiweb_version=2026.4.1&stiweb_imagesscalingfactor=1&stiweb_usecompression=true" +
"&stiweb_iconset=Office2022&stiweb_resourcesurl=x";
Parallel.For(0, 8, _ =>
{
for (int i = 0; i < 3000; i++)
{
try
{
StiBlazorComponent.GetRequestParams("https://host/Requests?x=" + i, post);
Interlocked.Increment(ref ok);
}
catch (Exception e)
{
errors.AddOrUpdate(e.GetType().Name + " @ " + e.StackTrace.Split('\n')[0].Trim(),
1, (_, n) => n + 1);
}
}
});
Console.WriteLine($"ok={ok}");
foreach (var kv in errors) Console.WriteLine($"{kv.Value} x {kv.Key}");Code: Select all
ok=8044
15150 x NullReferenceException @ at Stimulsoft.Report.Web.StiRequestParamsHelper.Get(HttpContext httpContext)
381 x ArgumentException @ at System.Collections.Hashtable.Insert(Object key, Object nvalue, Boolean add)
396 x InvalidOperationException @ at System.Collections.Specialized.NameObjectCollectionBase.NameObjectKeysEnumerator.MoveNext()
23 x NullReferenceException @ at System.Collections.Specialized.NameObjectCollectionBase.NameObjectKeysEnumerator.get_Current()
3 x IndexOutOfRangeException @ at System.Collections.ArrayList.Add(Object value)
3 x IndexOutOfRangeException @ at System.Collections.ArrayList.get_Item(Int32 index)Confirmation of the cause
Wrapping GetRequestParams in a process-wide lock (a Harmony prefix doing Monitor.Enter and a finalizer doing Monitor.Exit) makes the same test complete 24,000 of 24,000 calls with zero errors.