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的默认逻辑是:
- 会缓存整个响应内容,直到服务器发送完所有数据并主动关闭连接
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流式数据,不会再出现永久挂起的问题了。




