新版本 swagger 組件中 Servers 的 坑
新版本 Swashbuckle swagger 組件中 Servers 的 坑
Intro
上周做了公司的項目升級,從 2.2 更新到 3.1, swagger 直接更新到了最新,swagger 用的組件是 Swashbuckle.AspNetCore
,然後遇到一個 swagger 的問題,
在本地測試是沒問題的,但是部署在測試環境之後就會有問題,主要是 swagger 介面會多一個 servers 的選項,可能會導致 swagger 不能正常使用,下面詳細介紹一下
Swagger “bug” reproduce
大概的問題是這樣的,在本地環境是好的,在測試環境部署是有問題,測試環境部署之後的 swagger 介面大致如下:
很明顯這個 servers 是有問題的,我們實際訪問的地址是 //testserver/swagger
這樣的地址,但是 swagger 內部拼出來的 server 地址和實際訪問的地址是不符的,swagger 生成的 open api 文檔里也會有一個 servers 的屬性,示例如下:
這會導致我們使用 swagger 調試 API 的時候會走一個錯誤的 server 地址,實際請求的地址是 sever 地址加上 api path,可以看一個示例
Dig the Source
Swashbuckle.AspNetCore
是開源的,我們就是扒一扒它的實現源碼吧,我們用的是 5.6.3 版本,直接看 5.6.3 tag 對應的程式碼,可以找到 swagger 的中間件
在這裡我們可以看到,再返回給客戶端之前 open api 文檔響應之前我們是可以看到,是會經過 PreSerializeFilters
處理的,我們再詳細看一下 swaggerProvider.GetSwagger
的實現
實現程式碼在這裡(可以通過服務註冊找到對應的實現,也可以直接找對應介面的實現)
二者結合來看,servers 會根據用戶請求來獲取一個 server 地址,而當有 X-Forwarded-Host
請求頭的時候如果沒有按照 swagger 指定的規則這樣進行請求頭的轉發就會導致有問題,而我們的測試環境也正是因為如此,測試環境有一層 LB,經過 LB 轉發了 X-Forwarded-Host
和 X-Forwarded-Proto
請求頭,但是沒有轉發 X-Forwarded-Port
所以經過 swagger 的處理之後,就從 //testserver
變成了 //testserver:80
這樣
private string GetHostOrNullFromRequest(HttpRequest request)
{
if (!request.Headers.TryGetValue("X-Forwarded-Host", out StringValues forwardedHost))
return null;
var hostBuilder = new UriBuilder($"//{forwardedHost[0]}");
if (request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues forwardedProto))
hostBuilder.Scheme = forwardedProto[0];
if (request.Headers.TryGetValue("X-Forwarded-Port", out StringValues forwardedPort))
hostBuilder.Port = int.Parse(forwardedPort[0]);
return hostBuilder.Uri.ToString().Trim('/');
}
private string GetBasePathOrNullFromRequest(HttpRequest request)
{
var pathBuilder = new StringBuilder();
if (request.Headers.TryGetValue("X-Forwarded-Prefix", out StringValues forwardedPrefix))
pathBuilder.Append(forwardedPrefix[0].TrimEnd('/'));
if (request.PathBase.HasValue)
pathBuilder.Append(request.PathBase.Value.TrimEnd('/'));
return (pathBuilder.Length > 0)
? pathBuilder.ToString()
: null;
}
解決方案
從上面的源碼中基本就可以分析出問題的原因來,解決的辦法我覺得有下面幾種:
- LB 轉發的時候帶上
X-Forwarded-Port
請求頭,轉發原始請求的埠號(需要 LB 轉發自己能夠控制,我們如果要配置還需要讓 DevOps 的童鞋幫忙弄,如果完全是自己控制的就比較方便【推薦】) - 在使用 Swagger 中間件之前把
X-Forwarded-Port
請求頭設置為443
(不夠靈活,如果訪問 LB 是 http 或者有特別的埠號就會有問題) - 在使用 swagger 中間件之前把
X-Forwarded-Host
請求頭移除掉,這樣就不會有 servers 這個屬性了(感覺不夠優雅) - 註冊一個
PreSerializeFilter
把 Servers 清空,實現程式碼如下(【推薦】,沒有 servers 屬性的時候完全按請求 swagger 的 baseUrl 來作為 api 的前綴,示例程式碼如下)
app.UseSwagger(c =>
{
c.PreSerializeFilters.Add((doc, _) =>
{
doc.Servers?.Clear();
});
});
更新之後就沒有 servers 屬性了,和之前的版本保持一致了
More
我們使用的是 5.6.3 版本,應該從 5.6.0 開始都有這個問題,如果遇到了這個問題不要慌哈,參考上面的解決方案即可
我覺得 swagger 這樣的實現方式不太友好,更好的實現應該結合微軟的 ForwardHeaders
中間件來實現,Swagger 組件作者表示已經有計劃,打算在 6.0 的時候更新結合微軟的中間件來實現,詳細可以參考 Github 上的 Issue //github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1814
Reference
- //github.com/domaindrivendev/Swashbuckle.AspNetCore
- //github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/v5.6.3/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs
- //github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/v5.6.3/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L31
- //github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1814