MVC应用中Web API调用无响应问题排查求助
首先,最可能的原因是同步阻塞异步代码导致的死锁——这是传统ASP.NET(非Core)里开发者踩过无数次的异步坑。
死锁到底怎么来的?
你在Application_OnPostAuthenticateRequest这个同步事件方法里,用GetAwaiter().GetResult()强行阻塞等待异步任务完成。在传统ASP.NET中,每个请求都绑定了一个SynchronizationContext,当你用await但没加ConfigureAwait(false)时,异步操作完成后会试图回到这个上下文继续执行。但此时主线程已经被GetResult()卡死了,上下文被占着不放,异步任务没法回来,就形成了互相等待的死锁——你等我完成,我等你腾地方,谁都动不了。
快速验证这个问题
给所有await加上ConfigureAwait(false),绕开上下文捕获,修改后的代码如下:
public async Task<List<User>> GetUsersAsync(UserSearchRequest userSearchRequest) { using (var client = GetHttpClient(_baseUrl)) { // 加ConfigureAwait(false)避免捕获上下文 var response = await client.PostAsJsonAsync("api/v1/users/search", userSearchRequest).ConfigureAwait(false); if (response.IsSuccessStatusCode) { var users = await response.Content.ReadAsAsync<List<User>>().ConfigureAwait(false); return users; } // 别空着错误处理,至少抛个异常方便排查 throw new HttpRequestException($"API请求失败,状态码:{response.StatusCode}"); } } protected async Task SetUserRolesToSession() { var networkUserIdParts = networkUserId.Split(new char[] { '\'' }); var baseUrl = ConfigurationManager.AppSettings["SecurityApiBaseUrl"].ToString(); var userSearchRequest = new UserSearchRequest { NetworkUserId = Request.LogonUserIdentity.Name }; // 这里也加上ConfigureAwait(false) var user = (await new SecurityApiServiceWrapper(baseUrl) .GetUsersAsync(userSearchRequest)).ConfigureAwait(false).FirstOrDefault(); Session["Roles" + "_" + networkUserId] = user?.Groups.Select(x => x.Name).ToList(); }
如果改完不再卡住,那基本就是死锁的锅了。
还有其他可能的坑吗?
当然,除了死锁,这几个点也得排查:
1. Session访问时机不对
传统ASP.NET的请求生命周期里,PostAuthenticateRequest是在AcquireRequestState之前触发的,而Session要到AcquireRequestState阶段才初始化完成。你现在直接在PostAuthenticateRequest里写Session,搞不好会触发隐性的阻塞,甚至导致请求挂起。
2. 身份凭据不匹配
你的HttpClient开了UseDefaultCredentials = true,也就是用ASP.NET应用池的身份去访问Security API。但Postman里你可能手动用了有权限的用户,而应用池身份没权限,导致API那边卡在身份验证环节(比如NTLM协商超时),看起来就是你的请求卡住了。
3. HttpClient的配置问题
虽然你设了无限超时,但如果DNS解析失败、网络路由有问题,或者API端本身对特定客户端有拦截,也会导致请求发不出去。不过Postman能正常访问,这个概率低,但也不能完全排除。
一步步调试的方法
按这个顺序来排查,效率最高:
先解决死锁嫌疑:按上面的方法加
ConfigureAwait(false),测试是否恢复正常。如果好了,就确认是死锁问题,后续可以考虑把同步阻塞改成更优雅的异步处理(不过传统ASP.NET的Global.asax事件没有异步版本,所以ConfigureAwait(false)是比较实用的折中方案)。调整Session的访问时机:把写Session的逻辑移到
AcquireRequestState事件里,这个事件是专门处理Session初始化的,绝对安全。修改Global.asax:
protected void Application_AcquireRequestState(object sender, EventArgs e) { // 先判断Session里有没有值,避免重复请求API var networkUserId = // 这里补上你的networkUserId获取逻辑 if (Session["Roles_" + networkUserId] == null) { SetUserRolesToSession().GetAwaiter().GetResult(); } }
核对身份凭据:
- 输出
Request.LogonUserIdentity.Name的值,看看是不是和你Postman里用的用户一致。 - 临时把ASP.NET应用池的身份改成你Postman用的用户,测试能不能正常调用API。如果能,就是应用池身份权限的问题。
- 输出
抓包看请求细节:
- 用Fiddler或者Wireshark抓一下应用程序发的请求,和Postman的请求对比:
- 检查URL、请求体是不是完全一样。
- 看请求头里的身份验证信息(比如NTLM的协商过程)有没有问题。
- 确认请求到底发没发出去,有没有收到响应。
- 用Fiddler或者Wireshark抓一下应用程序发的请求,和Postman的请求对比:
单独测试HttpClient代码:
- 写个简单的控制台程序,复制你的
SecurityApiServiceWrapper代码,调用同一个API。如果控制台程序能正常返回,说明问题出在ASP.NET的上下文环境里(死锁、Session、身份上下文这些)。
- 写个简单的控制台程序,复制你的
查API端的日志:
- 找维护Security API的同事,看看他们的日志里有没有收到你的应用程序的请求,以及请求的处理状态。如果API端根本没收到请求,那问题在你的请求发送环节;如果收到了但没响应,那就是API端的问题。
内容的提问来源于stack exchange,提问作者user9393635

