ASP.NET Web API中OwinMiddleware无法修改SignalR协商/启动请求缓存头
我编写了如下Owin中间件代码,旨在将HTTP缓存控制头统一设置为no-cache, no-store, must-revalidate:
public class CacheMiddleware : OwinMiddleware { public CacheMiddleware(OwinMiddleware next) : base(next) { } public override async Task Invoke(IOwinContext context) { context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; context.Response.Headers["Pragma"] = "no-cache"; context.Response.Headers["Expires"] = "0"; context.Response.Headers.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains;"); await Next.Invoke(context); } }
但实际运行后发现:Chrome开发者工具中,SignalR的signalr/negotiate和signalr/start请求响应头仅返回Cache-Control:no-cache,而signalr/connect及其他普通请求则能正常应用目标缓存头。
两类请求的响应头对比:
signalr/negotiate & signalr/start响应头:
Cache-Control:no-cache Connection:keep-alive Host:10.0.211.202 Pragma:no-cache Expires:-1 Strict-Transport-Security:max-age=31536000; includeSubDomains;signalr/connect响应头:
Cache-Control:no-cache, no-store, must-revalidate Connection:Upgrade Strict-Transport-Security:max-age=31536000; includeSubDomains; Upgrade:websocket
原因分析
SignalR的negotiate和start请求属于握手关键请求,其内部处理逻辑(如NegotiateHandler)会在你的中间件执行之后运行,并且会主动覆盖Cache-Control、Expires等缓存头,强制设置为no-cache和Expires:-1——这是SignalR的内置机制,目的是避免浏览器缓存握手结果,防止后续连接出现异常。
而signalr/connect是WebSocket或长连接的建立请求,SignalR内部没有对这类请求的缓存头做强制覆盖,所以中间件设置的头能正常保留。
解决方案
要让negotiate和start请求也应用自定义缓存头,核心思路是在SignalR内部处理完成后,再重写响应头,具体有两种实现方式:
1. 全局后置修改缓存头
调整中间件逻辑,先让后续中间件(包括SignalR)处理请求,再设置/覆盖缓存头:
public class CacheMiddleware : OwinMiddleware { public CacheMiddleware(OwinMiddleware next) : base(next) { } public override async Task Invoke(IOwinContext context) { // 先执行后续中间件,包括SignalR的处理逻辑 await Next.Invoke(context); // 后置修改缓存头,覆盖SignalR的默认设置 context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; context.Response.Headers["Pragma"] = "no-cache"; context.Response.Headers["Expires"] = "0"; context.Response.Headers.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains;"); } }
2. 针对SignalR路径单独处理
如果不想全局修改所有请求的处理顺序,可以在中间件中判断请求路径,仅对negotiate/start请求做后置覆盖:
public class CacheMiddleware : OwinMiddleware { public CacheMiddleware(OwinMiddleware next) : base(next) { } public override async Task Invoke(IOwinContext context) { var requestPath = context.Request.Path.Value; // 先执行后续中间件 await Next.Invoke(context); // 对SignalR握手请求重写缓存头 if (requestPath.Contains("/signalr/negotiate") || requestPath.Contains("/signalr/start")) { context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; context.Response.Headers["Pragma"] = "no-cache"; context.Response.Headers["Expires"] = "0"; } // 其他请求保持自定义缓存设置 else { context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; context.Response.Headers["Pragma"] = "no-cache"; context.Response.Headers["Expires"] = "0"; } // HSTS头全局生效 context.Response.Headers.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains;"); } }
验证效果
修改代码后重新部署服务,在Chrome开发者工具中查看signalr/negotiate和signalr/start的响应头,即可看到自定义的Cache-Control:no-cache, no-store, must-revalidate已生效。
内容的提问来源于stack exchange,提问作者Shilpa Bharkhada

