You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Blazor WebAssembly调用HttpClient.GetStreamAsync消费.NET 10 Server-Sent Events时永久挂起的问题排查与解决

Blazor WebAssembly调用HttpClient.GetStreamAsync消费.NET 10 Server-Sent Events时永久挂起的问题排查与解决

我碰到过好几个类似的Blazor WASM + SSE的坑,你这个情况核心原因是Blazor WASM的HttpClient默认行为和浏览器Fetch API的限制导致的,咱们一步步拆解解决:

一、为什么会永久挂起?

Blazor WebAssembly的HttpClient底层基于浏览器的Fetch API实现,而Fetch API的默认逻辑是:

  1. 会缓存整个响应内容,直到服务器发送完所有数据并主动关闭连接
  2. GetStreamAsync方法默认使用HttpCompletionOption.ResponseContentRead,也就是必须等整个响应体接收完成才会返回流

但SSE是持续的流式响应,服务器会一直保持连接并不断推送数据,永远不会主动关闭连接,所以GetStreamAsync会一直等待响应"完成",自然就永久挂起了。

二、快速解决方法:改用HttpCompletionOption.ResponseHeadersRead

只需要修改客户端的请求方式,告诉HttpClient在收到响应头后就开始处理响应体,而不是等整个响应结束:

修改后的客户端代码

using var httpClient = new HttpClient();
var url = $"/ssehost/connect/{WebUtility.UrlEncode(ServiceId.ToString())}";

// 改用SendAsync并指定ResponseHeadersRead
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("SessionId", SessionId.ToString());

// 关键:指定ResponseHeadersRead,收到响应头后就返回
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Cts.Token);
response.EnsureSuccessStatusCode(); // 确保请求成功

using var stream = await response.Content.ReadAsStreamAsync(Cts.Token);
using var streamReader = new StreamReader(stream, Encoding.UTF8); // 显式指定UTF-8(SSE默认编码)

var buffer = new StringBuilder();
var chunk = new char[1024];

while (!Cts.IsCancellationRequested)
{
    var read = await streamReader.ReadAsync(chunk.AsMemory(0, chunk.Length), Cts.Token);
    if (read <= 0) break;

    buffer.Append(chunk, 0, read);

    while (TryExtractFrame(buffer, out var frame))
    {
        ParseFrame(frame);
    }
}

三、额外注意事项

1. 后端SSE格式验证

你用Results.ServerSentEvents是正确的,但要确保SseHost.ReadAllAsync()返回的是IAsyncEnumerable<ServerSentEvent>,ASP.NET Core会自动将其转换为符合SSE规范的帧(比如以data: 开头、\n\n分隔)。示例后端生成逻辑:

public async IAsyncEnumerable<ServerSentEvent> ReadAllAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        // 模拟获取业务数据
        var eventData = await FetchNextEventAsync(cancellationToken);
        // 生成标准SSE帧
        yield return new ServerSentEvent(eventData);
        // 带事件类型的帧示例:yield return new ServerSentEvent { Event = "status-update", Data = eventData };
    }
}

2. Blazor主线程阻塞问题

Blazor WASM是单线程模型,长时间在循环中处理流式数据可能会阻塞UI渲染。建议把SSE处理逻辑放到后台线程,更新UI时切回主线程:

// 在组件中启动后台任务
_ = Task.Run(async () =>
{
    while (!Cts.IsCancellationRequested)
    {
        // 读取和处理SSE帧的逻辑...
        
        // 更新UI必须切换回主线程
        await InvokeAsync(() =>
        {
            // 更新组件状态
            StateHasChanged();
        });
    }
}, Cts.Token);

3. CORS配置检查

确保后端CORS政策允许自定义头SessionId及流式请求:

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowSSE", policy =>
    {
        policy.WithOrigins("你的Blazor WASM域名")
              .AllowAnyMethod()
              .AllowHeaders("SessionId") // 显式允许自定义头
              .AllowCredentials(); // 若需携带认证信息
    });
});

4. 替代方案:使用浏览器原生EventSource

如果HttpClient流式处理仍有问题,可直接用浏览器专为SSE设计的EventSourceAPI(JS互调),兼容性更可靠:

// Blazor组件中调用JS创建EventSource
var eventSource = await JSRuntime.InvokeAsync<IJSObjectReference>(
    "createSseConnection", 
    url, 
    new { SessionId = SessionId.ToString() });

// 监听SSE消息事件
await eventSource.InvokeVoidAsync("addEventListener", "message", (Action<object>)(message =>
{
    var frameData = message.ToString();
    ParseFrame(frameData);
    InvokeAsync(StateHasChanged);
}));

// 监听错误事件
await eventSource.InvokeVoidAsync("addEventListener", "error", (Action)(() =>
{
    // 处理连接断开/错误逻辑
}));

对应的wwwroot/js/sse.js代码:

window.createSseConnection = function(url, headers) {
    const eventSource = new EventSource(url, { headers });
    return eventSource;
};

按照上面的调整,你的Blazor WASM客户端应该就能正常接收SSE流式数据,不会再出现永久挂起的问题了。

火山引擎 最新活动