接獲報案,某ASP.NET MVC網站出現奇妙現象:MVC Controller/View功能正常,存取WebAPI ApiController則傳回HTTP狀態500(內部伺服器錯誤),但除了狀態碼沒有其他錯誤訊息。

原以為該Web API Controller程式有錯,進一步測試其他Controller,發現非Web API的Controller都正常,URL只要符合"/api/controller/id",就會傳回沒有錯誤訊息的HTTP 500,甚至胡亂輸入不存在的/api/ooo/xxx也會傳回HTTP 500(正常狀況應傳回No type was found that matches the controller named 'ooo'),檢查事件檢視器沒有任何ASP.NET/IIS相關錯誤。至此,案情陷入膠著…

冷靜檢視線索,由「只要URL符合/api/controller/id格式,不管Controller存在與否,一律傳回HTTP 500」的行為推斷,問題可能發生在路由解析階段,但少了錯誤訊息就像矇眼射箭,找不到頭緒,故首要之急得快點找出詳細錯誤訊息。

爬文才知,Web API出錯時不是依web.config customErrors決定是否回傳錯誤訊息,而是另一套客製化錯誤設定-GlobalConfiguration.Configuration.IncludeErrorDetailPolicy。如同customErros,一樣有LocalOnly、Always、Off三種模式,推測該設定被設為LocalOnly,故由遠端瀏覽只有HTTP500,沒有錯誤細節。Terminal Service登入問題主機,瀏覽localhost/api/ooo/xxx,總算揭開神祕面紗:

HTTP/1.1 500 Internal Server Error
Date: Tue, 10 Mar 2015 10:08:33 GMT
Server: Microsoft-IIS/6.0
X-Powered-By: ASP.NET
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8

<Exception>
<ExceptionType>System.IO.FileNotFoundException</ExceptionType>
<Message>
Could not load file or assembly 'Autofac, Version=3.0.0.0, Culture=neutral, PublicKeyToken=17863af14b0044da' or one of its dependencies. The system cannot find the file specified.
</Message>
<StackTrace>
Server stack trace:
   at System.Reflection.RuntimeAssembly.GetExportedTypes(RuntimeAssembly assembly, ObjectHandleOnStack retTypes)
   at System.Reflection.RuntimeAssembly.GetExportedTypes()
   at System.Web.Http.Dispatcher.DefaultHttpControllerTypeResolver.GetControllerTypes(IAssembliesResolver assembliesResolver)
   at System.Web.Http.WebHost.WebHostHttpControllerTypeResolver.GetControllerTypes(IAssembliesResolver assembliesResolver)
   at System.Web.Http.Dispatcher.HttpControllerTypeCache.InitializeCache()
   at System.Lazy`1.CreateValue()
Exception rethrown at [0]:
   at System.Reflection.RuntimeAssembly.GetExportedTypes(RuntimeAssembly assembly, ObjectHandleOnStack retTypes)
   at System.Reflection.RuntimeAssembly.GetExportedTypes()
   at System.Web.Http.Dispatcher.DefaultHttpControllerTypeResolver.GetControllerTypes(IAssembliesResolver assembliesResolver)
   at System.Web.Http.WebHost.WebHostHttpControllerTypeResolver.GetControllerTypes(IAssembliesResolver assembliesResolver)
   at System.Web.Http.Dispatcher.HttpControllerTypeCache.InitializeCache()
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.LazyInitValue()
   at System.Lazy`1.get_Value()
   at System.Web.Http.Dispatcher.DefaultHttpControllerSelector.InitializeControllerInfoCache()
   at System.Lazy`1.CreateValue()
Exception rethrown at [1]:
   at System.Reflection.RuntimeAssembly.GetExportedTypes(RuntimeAssembly assembly, ObjectHandleOnStack retTypes)
   at System.Reflection.RuntimeAssembly.GetExportedTypes()
   at System.Web.Http.Dispatcher.DefaultHttpControllerTypeResolver.GetControllerTypes(IAssembliesResolver assembliesResolver)
   at System.Web.Http.WebHost.WebHostHttpControllerTypeResolver.GetControllerTypes(IAssembliesResolver assembliesResolver)
   at System.Web.Http.Dispatcher.HttpControllerTypeCache.InitializeCache()
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.LazyInitValue()
   at System.Lazy`1.get_Value()
   at System.Web.Http.Dispatcher.DefaultHttpControllerSelector.InitializeControllerInfoCache()
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.LazyInitValue()
   at System.Lazy`1.get_Value()
   at System.Web.Http.Dispatcher.DefaultHttpControllerSelector.SelectController(HttpRequestMessage request)
   at System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsyncInternal(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
</StackTrace>
</Exception>

訊息明確,Autofac版本不對!專案已升級到3.0版,問題主機的Autofac.dll漏更新,還是2.2版,只需更新版本就可解決。

問題排除了,但我有疑問:為什麼問題只發生在Web API Controller? 決定追根究底。

由訊息WebHostHttpControllerTypeResolver跟Autofac關鍵字,直覺已被設定用Autofac解析Controller型別。只是在程式碼中挖了很久,怎麼都找不出指定由Autofac解析Controller的設定(標準做法是指定config.DependencyResolver,參考),一度以為是某種沒看過的巫術。迷惘了一陣子,回頭重看錯誤訊息Callstack,這才發現,或許Autofac不是用來解析Controller型別,先前完全查錯方向。

試著將專案移到我的Windows 8.1執行打算重現問題仔細研究,奇妙的事發生了!在我的機器上即使Autofac.dll被刪除也不會出錯!同事的Windows 7則能重現Autofac版本不符就爆的結果,再把檔案移到Windows 2003 VM測試對照,也能重現問題。這下子又多一項待解疑惑:Autofac版本不符錯誤只發生在Windows 2003/Windows 7,在Windows 8不會出現。

更!為了解開一個謎團,生出第二個謎團,這茶包會細胞分裂來著~(捏碎滑鼠)

為縮小問題範圍,祭出「消去法」,將專案非必要檔案一一移除,只求能重現/api/ooo/xxx這種亂打URL可以傳回No type was found that matches the controller named 'ooo'的行為即可。意外發現,在移除/bin下的Foo.dll、Bar.dll後,即使沒有Autofac.dll,/api/ooo/xxx的回應也是正常。而這兩顆DLL的關聯是:Foo.dll參照Bar.dll,Bar.dll再參照Autofac.dll。進一步測試,若只刪除Bar.dll,我會得到Could not load file or assembly 'Bar, Version=1.0…'錯誤。由此推測,Autofac版本不符錯誤來自Foo.dll的間接引用,與解析Controller名稱的DI(Dependency Injection)機制無關。

回頭重看Callstack,我注意到一個方法:System.Web.Http.Dispatcher.HttpControllerTypeCache.InitializeCache()。由此推測:每次存取/api/*時,MVC會掃瞄bin資料夾所有DLL,建立型別Cache。在處理Foo.dll時,先參照Bar.dll、Bar.dll又參照Autofac.dll,發生版本不符導致Cache無法建立,造成/api/*解析失敗,所有/api/*存取一律傳回HTTP 500。

/api/*路由失效有了合理解釋,但,為什麼一般MVC功能正常,只有執行Web API出錯?

config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);
 
routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

比較Web API與一般MVC路由註冊方式,發現Web API使用MapHttpRoute,MVC則是MapRoute,猜想兩種註冊方式背後運作的方式不同,是導致MVC功能OK,Web API失敗的原因。

最後一個謎:為什麼同樣的程式碼在Windows 7/2003會出錯,在Windows 8不會?

為此反組譯查出Callstack中出現的GetExportedTypes(RuntimeAssembly assembly, ObjectHandleOnStack retTypes),其實是個Unmanaged方法,實際函式在clr.dll(DllImport("QCall")是個奇妙註記,沒有所謂QCall.dll,而是指向放在.NET Runtime clr.dll的外部函式。參考):

[DllImport("QCall", CharSet=CharSet.Unicode, ExactSpelling=false)]
[SecurityCritical]
[SuppressUnmanagedCodeSecurity]
private static extern void GetExportedTypes(RuntimeAssembly assembly, ObjectHandleOnStack retTypes);

而Windows 7與Windows 8未共用clr.dll,因實做方式不同,各有自己專屬的版本(二者版號也不同:4.0.30319.18408 vs 4.0.30319.33440。參考),應是執行結果不同的原因。

所有的謎團都有了合理解釋,總算甘心合上茶包檔案夾,Case Closed。


Comments

# by PETER

請問: 用外部不URL加入WS後,測試都出現500錯誤。 一般建立WebService網站後,在本機端用localhost去加入使用 ,用json傳回,有上nuget管理員下載關於DataTable轉json的Json.NET套件,這顯示都ok。 一旦,測試網站用實體外部IP,像是 http://電腦名稱/ws網站名稱/wsxxxx.asmx IIS6.1 / Windows 7專業版 去加入後,在除錯時,都會出現500錯誤。 遠端伺服器傳回一個錯誤: (500) 內部伺服器錯誤

# by Jeffrey

to PETER, 建議設法取得HTTP 500錯誤細節(修改web.config customErrors設定,IE關閉「顯示易懂的HTTP錯誤訊息」),以釐清下一步調查方向。

Post a comment