問題發生點在於我有個 Controller 裡的 Action 如以下
```C#
[HttpGet("[controller]/[action]/{id:int}/{myUrl?}")]
public async Task<IActionResult> Handler(long id, string myUrl)
{
/* 略 */
return Content("OK");
}
```
使用的 ASP.NET Core 版本為 2.2,首先我在本地端伺服器 Kestrel 執行網址如下
### `https://localhost:5001/Test/Handler/100/https%3A%2F%2Flocalhost%3A5001%2FSomePath%2F20`
### `參數 id 使用 100` ### `參數 myUrl 使用 UrlEncode 過的網址(https://localhost:5001/SomePath/20)` 測時結果正常可呼叫到我預期的 Handler Action,接著我把該程式發佈到 IIS 上後,在執行一次同樣的路徑,卻跳出了 404 找不到該路徑的錯誤頁面如下... ![Image](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiqnilUzpCr55UnUNplHXO-sc6FgHOGpMH2p_OsWqEzUdN4xNNfsiL7dTxg2YzUYQGBTqtq4qF3FuIj133gIosy_VtarmmvPbiMfKRwWCyFbSg7ekxBXMWqV525PGPPf3gymu_2DrMJ5C7c/s1600/4.jpg) 經過一段時間在網路上亂試亂寫的結果後,才知道原來 IIS 會自動先將網址做 UrlDecode 的動作,造成 Client 端向 Server 端要求的路徑在 Route 裡匹配不到, 則我以為 IIS 端收到交給應用程式處理的網址為 ### `https://mydomain.com/Test/Handler/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` 但實際上 IIS 端收到交給應用程式處理的網址確是 ### `https://mydomain.com/Test/Handler/5/https://mydomain.com/SomePath/20` 原定的 Handler Action 裡只有處理兩個參數而已,而先被 UrlDecode 過的網址要求已經不止兩個參數了...看了很多文章都是設定 web.config 檔裡的 rewrite 區段,由於有點複雜且只適用於 IIS,所以個人傾向不採用選擇了其它解決方式,而產生了這篇文章。 採取的解決方式為只要將路由設定裡的 `{myUrl?}` 更改為 `{**myUrl}` 即 catch-all 將剩下的路徑視為單一個參數,但在[微軟官方裡的文章](https://docs.microsoft.com/en-gb/aspnet/core/fundamentals/routing?view=aspnetcore-2.2#differences-from-earlier-versions-of-routing)有提到加一個星號或兩個星號都可以,差異是在於單星號會將參數裡的斜線(/)編碼,然後雙星號只有在 ASP.NET Core 2.2 版本之後才開始支援。 初步大致上了解兩者使用情境的差別,但還是想知道兩者在系統實際運行上表現的方式,所以做了一些簡單的測試,讓之後忘記時還可以回來參考。 **測試 Action 如下,然後分別在 Kestrel 和 IIS 兩邊進行觀察** 單星號(*) Action ```C# [HttpGet("[controller]/[action]/{id:int}/{*myUrl}")] public async Task<IActionResult> Handler(long id, string myUrl) { /* 略 */ return Content("OK"); } ``` 雙星號(**) Action ```C# [HttpGet("[controller]/[action]/{id:int}/{**myUrl}")] public async Task<IActionResult> Handler2(long id, string myUrl) { /* 略 */ return Content("OK"); } ``` 1. 比較使用 Url.Action() 生成的網址 Kestrel (單星號*): Input #### `1. Url.Action(nameof(Handler), "Test", new { id = 5, myUrl="https%3A%2F%2Fmydomain.com%2FSomePath%2F20"}, Request.Scheme, Request.Host.Value)`
#### `2. Url.Action(nameof(Handler), "Test", new { id = 5, myUrl="https://mydomain.com/SomePath/20"}, Request.Scheme, Request.Host.Value)` Output #### `1. https://localhost:5001/Test/Handler/5?myUrl=https%253A%252F%252Fmydomain.com%252FSomePath%252F20` #### `2. https://localhost:5001/Test/Handler/5?myUrl=https%3A%2F%2Fmydomain.com%2FSomePath%2F20` IIS (單星號*): Input #### `1. Url.Action(nameof(Handler), "Test", new { id = 5, myUrl="https%3A%2F%2Fmydomain.com%2FSomePath%2F20"}, Request.Scheme, Request.Host.Value)`
#### `2. Url.Action(nameof(Handler), "Test", new { id = 5, myUrl="https://mydomain.com/SomePath/20"}, Request.Scheme, Request.Host.Value)` Output #### `1. https://mydomain.com/Test/Handler/5?myUrl=https%253A%252F%252Fmydomain.com%252FSomePath%252F20` #### `2. https://mydomain.com/Test/Handler/5?myUrl=https%3A%2F%2Fmydomain.com%2FSomePath%2F20` Kestrel (雙星號**): Input #### `1. Url.Action(nameof(Handler2), "Test", new { id = 5, myUrl="https%3A%2F%2Fmydomain.com%2FSomePath%2F20"}, Request.Scheme, Request.Host.Value)`
#### `2. Url.Action(nameof(Handler2), "Test", new { id = 5, myUrl="https://mydomain.com/SomePath/20"}, Request.Scheme, Request.Host.Value)` Output #### `1. https://localhost:5001/Test/Handler2/5?myUrl=https%253A%252F%252Fmydomain.com%252FSomePath%252F20` #### `2. https://localhost:5001/Test/Handler2/5?myUrl=https%3A%2F%2Fmydomain.com%2FSomePath%2F20` IIS (雙星號**): Input #### `1. Url.Action(nameof(Handler2), "Test", new { id = 5, myUrl="https%3A%2F%2Fmydomain.com%2FSomePath%2F20"}, Request.Scheme, Request.Host.Value)`
#### `2. Url.Action(nameof(Handler2), "Test", new { id = 5, myUrl="https://mydomain.com/SomePath/20"}, Request.Scheme, Request.Host.Value)` Output #### `1. https://mydomain.com/Test/Handler2/5?myUrl=https%253A%252F%252Fmydomain.com%252FSomePath%252F20` #### `2. https://mydomain.com/Test/Handler2/5?myUrl=https%3A%2F%2Fmydomain.com%2FSomePath%2F20` 可以發現生成的網址在兩個不同的平台上並無差異,值得注意的是使用 `Url.Action()` 會自動幫你的參數 UrlEncode 一次,所以就不用再自己做 UrlEncode 處理,否則某些場合會造成 Double UrlEncode 的問題。 2. 比較參數含有網址字串的要求 Kestrel (單星號*): Input #### `1. https://localhost:5001/Test/Handler/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` #### `2. https://localhost:5001/Test/Handler/5/https://mydomain.com/SomePath/20` Output #### `1. https://localhost:5001/Test/Handler/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` #### `2. https://localhost:5001/Test/Handler/5/https://mydomain.com/SomePath/20` IIS (單星號*): Input #### `1. https://mydomain.com/Test/Handler/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` #### `2. https://mydomain.com/Test/Handler/5/https://mydomain.com/SomePath/20` Output #### `1. https://mydomain.com/Test/Handler/5/https://mydomain.com/SomePath/20` #### `2. https://mydomain.com/Test/Handler/5/https://mydomain.com/SomePath/20` Kestrel (雙星號**): Input #### `1. https://localhost:5001/Test/Handler2/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` #### `2. https://localhost:5001/Test/Handler2/5/https://mydomain.com/SomePath/20` Output #### `1. https://localhost:5001/Test/Handler2/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` #### `2. https://localhost:5001/Test/Handler2/5/https://mydomain.com/SomePath/20` IIS (雙星號**): Input #### `1. https://mydomain.com/Test/Handler2/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` #### `2. https://mydomain.com/Test/Handler2/5/https://mydomain.com/SomePath/20` Output #### `1. https://mydomain.com/Test/Handler2/5/https://mydomain.com/SomePath/20` #### `2. https://mydomain.com/Test/Handler2/5/https://mydomain.com/SomePath/20` 可以發現 IIS 在接收要求的參數時,確實會先將網址參數做 UrlDecode,而 Kestrel 則保留原始參數的編碼直接 Pass 到 Action 裡,但是不知道什麼原因,不管使用單星號或雙星號的方式所得到的測試結果並無差異,不像微軟文章所說的產生連結時雙星號會保留路徑(/)分隔符號字元,所以目前的認知是都先採用雙星號(**)為主。 ## 總結: 1\. 使用 Url.Action() 生成的網址在兩個不同的平台上沒有差異,然後 Url.Action() 會自動幫你的參數部分先 UrlEncode 一次,所以可以省去自己做 UrlEncode 處理。
2\. IIS 在接收要求的參數時,會先將網址參數做 UrlDecode,而 Kestrel 則保留原始參數的編碼直接 Pass 到 Action 裡。
3\. 自定路由 Template 的參數前綴符號不管使用單一星號(\*)或雙星號(\*\*)所得到的測試結果皆相同。 ## 補充: 在 IIS 上如果要使用含有 UrlEncode 過的網址參數呼叫 Action 時,接到的參數會少一個 `/`,例如傳 https://linmasaki09.blogspot.com/https%3A%2F%2Fwww.abc.com.tw,則接到的參數值會變為 https://linmasaki09.blogspot.com/https:/www.abc.com.tw,目前的解法是使用 IIS URL Rewrite 模組(沒有需先安裝)在 `web.config` 裡加上以下設定即可解決 ``` Xml
.......... Remove other settings for brevity ..........
// 1. 在 requestFiltering 裡加上 allowDoubleEscaping="true"
// 2. 新增下面的 Rewrite Rule
.......... Remove other settings for brevity ..........
```
##參考資料 [\[MSDN\] Routing in ASP.NET Core](https://docs.microsoft.com/en-gb/aspnet/core/fundamentals/routing?view=aspnetcore-2.2#differences-from-earlier-versions-of-routing)
### `參數 id 使用 100` ### `參數 myUrl 使用 UrlEncode 過的網址(https://localhost:5001/SomePath/20)` 測時結果正常可呼叫到我預期的 Handler Action,接著我把該程式發佈到 IIS 上後,在執行一次同樣的路徑,卻跳出了 404 找不到該路徑的錯誤頁面如下... ![Image](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiqnilUzpCr55UnUNplHXO-sc6FgHOGpMH2p_OsWqEzUdN4xNNfsiL7dTxg2YzUYQGBTqtq4qF3FuIj133gIosy_VtarmmvPbiMfKRwWCyFbSg7ekxBXMWqV525PGPPf3gymu_2DrMJ5C7c/s1600/4.jpg) 經過一段時間在網路上亂試亂寫的結果後,才知道原來 IIS 會自動先將網址做 UrlDecode 的動作,造成 Client 端向 Server 端要求的路徑在 Route 裡匹配不到, 則我以為 IIS 端收到交給應用程式處理的網址為 ### `https://mydomain.com/Test/Handler/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` 但實際上 IIS 端收到交給應用程式處理的網址確是 ### `https://mydomain.com/Test/Handler/5/https://mydomain.com/SomePath/20` 原定的 Handler Action 裡只有處理兩個參數而已,而先被 UrlDecode 過的網址要求已經不止兩個參數了...看了很多文章都是設定 web.config 檔裡的 rewrite 區段,由於有點複雜且只適用於 IIS,所以個人傾向不採用選擇了其它解決方式,而產生了這篇文章。 採取的解決方式為只要將路由設定裡的 `{myUrl?}` 更改為 `{**myUrl}` 即 catch-all 將剩下的路徑視為單一個參數,但在[微軟官方裡的文章](https://docs.microsoft.com/en-gb/aspnet/core/fundamentals/routing?view=aspnetcore-2.2#differences-from-earlier-versions-of-routing)有提到加一個星號或兩個星號都可以,差異是在於單星號會將參數裡的斜線(/)編碼,然後雙星號只有在 ASP.NET Core 2.2 版本之後才開始支援。 初步大致上了解兩者使用情境的差別,但還是想知道兩者在系統實際運行上表現的方式,所以做了一些簡單的測試,讓之後忘記時還可以回來參考。 **測試 Action 如下,然後分別在 Kestrel 和 IIS 兩邊進行觀察** 單星號(*) Action ```C# [HttpGet("[controller]/[action]/{id:int}/{*myUrl}")] public async Task<IActionResult> Handler(long id, string myUrl) { /* 略 */ return Content("OK"); } ``` 雙星號(**) Action ```C# [HttpGet("[controller]/[action]/{id:int}/{**myUrl}")] public async Task<IActionResult> Handler2(long id, string myUrl) { /* 略 */ return Content("OK"); } ``` 1. 比較使用 Url.Action() 生成的網址 Kestrel (單星號*): Input #### `1. Url.Action(nameof(Handler), "Test", new { id = 5, myUrl="https%3A%2F%2Fmydomain.com%2FSomePath%2F20"}, Request.Scheme, Request.Host.Value)`
#### `2. Url.Action(nameof(Handler), "Test", new { id = 5, myUrl="https://mydomain.com/SomePath/20"}, Request.Scheme, Request.Host.Value)` Output #### `1. https://localhost:5001/Test/Handler/5?myUrl=https%253A%252F%252Fmydomain.com%252FSomePath%252F20` #### `2. https://localhost:5001/Test/Handler/5?myUrl=https%3A%2F%2Fmydomain.com%2FSomePath%2F20` IIS (單星號*): Input #### `1. Url.Action(nameof(Handler), "Test", new { id = 5, myUrl="https%3A%2F%2Fmydomain.com%2FSomePath%2F20"}, Request.Scheme, Request.Host.Value)`
#### `2. Url.Action(nameof(Handler), "Test", new { id = 5, myUrl="https://mydomain.com/SomePath/20"}, Request.Scheme, Request.Host.Value)` Output #### `1. https://mydomain.com/Test/Handler/5?myUrl=https%253A%252F%252Fmydomain.com%252FSomePath%252F20` #### `2. https://mydomain.com/Test/Handler/5?myUrl=https%3A%2F%2Fmydomain.com%2FSomePath%2F20` Kestrel (雙星號**): Input #### `1. Url.Action(nameof(Handler2), "Test", new { id = 5, myUrl="https%3A%2F%2Fmydomain.com%2FSomePath%2F20"}, Request.Scheme, Request.Host.Value)`
#### `2. Url.Action(nameof(Handler2), "Test", new { id = 5, myUrl="https://mydomain.com/SomePath/20"}, Request.Scheme, Request.Host.Value)` Output #### `1. https://localhost:5001/Test/Handler2/5?myUrl=https%253A%252F%252Fmydomain.com%252FSomePath%252F20` #### `2. https://localhost:5001/Test/Handler2/5?myUrl=https%3A%2F%2Fmydomain.com%2FSomePath%2F20` IIS (雙星號**): Input #### `1. Url.Action(nameof(Handler2), "Test", new { id = 5, myUrl="https%3A%2F%2Fmydomain.com%2FSomePath%2F20"}, Request.Scheme, Request.Host.Value)`
#### `2. Url.Action(nameof(Handler2), "Test", new { id = 5, myUrl="https://mydomain.com/SomePath/20"}, Request.Scheme, Request.Host.Value)` Output #### `1. https://mydomain.com/Test/Handler2/5?myUrl=https%253A%252F%252Fmydomain.com%252FSomePath%252F20` #### `2. https://mydomain.com/Test/Handler2/5?myUrl=https%3A%2F%2Fmydomain.com%2FSomePath%2F20` 可以發現生成的網址在兩個不同的平台上並無差異,值得注意的是使用 `Url.Action()` 會自動幫你的參數 UrlEncode 一次,所以就不用再自己做 UrlEncode 處理,否則某些場合會造成 Double UrlEncode 的問題。 2. 比較參數含有網址字串的要求 Kestrel (單星號*): Input #### `1. https://localhost:5001/Test/Handler/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` #### `2. https://localhost:5001/Test/Handler/5/https://mydomain.com/SomePath/20` Output #### `1. https://localhost:5001/Test/Handler/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` #### `2. https://localhost:5001/Test/Handler/5/https://mydomain.com/SomePath/20` IIS (單星號*): Input #### `1. https://mydomain.com/Test/Handler/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` #### `2. https://mydomain.com/Test/Handler/5/https://mydomain.com/SomePath/20` Output #### `1. https://mydomain.com/Test/Handler/5/https://mydomain.com/SomePath/20` #### `2. https://mydomain.com/Test/Handler/5/https://mydomain.com/SomePath/20` Kestrel (雙星號**): Input #### `1. https://localhost:5001/Test/Handler2/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` #### `2. https://localhost:5001/Test/Handler2/5/https://mydomain.com/SomePath/20` Output #### `1. https://localhost:5001/Test/Handler2/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` #### `2. https://localhost:5001/Test/Handler2/5/https://mydomain.com/SomePath/20` IIS (雙星號**): Input #### `1. https://mydomain.com/Test/Handler2/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20` #### `2. https://mydomain.com/Test/Handler2/5/https://mydomain.com/SomePath/20` Output #### `1. https://mydomain.com/Test/Handler2/5/https://mydomain.com/SomePath/20` #### `2. https://mydomain.com/Test/Handler2/5/https://mydomain.com/SomePath/20` 可以發現 IIS 在接收要求的參數時,確實會先將網址參數做 UrlDecode,而 Kestrel 則保留原始參數的編碼直接 Pass 到 Action 裡,但是不知道什麼原因,不管使用單星號或雙星號的方式所得到的測試結果並無差異,不像微軟文章所說的產生連結時雙星號會保留路徑(/)分隔符號字元,所以目前的認知是都先採用雙星號(**)為主。 ## 總結: 1\. 使用 Url.Action() 生成的網址在兩個不同的平台上沒有差異,然後 Url.Action() 會自動幫你的參數部分先 UrlEncode 一次,所以可以省去自己做 UrlEncode 處理。
2\. IIS 在接收要求的參數時,會先將網址參數做 UrlDecode,而 Kestrel 則保留原始參數的編碼直接 Pass 到 Action 裡。
3\. 自定路由 Template 的參數前綴符號不管使用單一星號(\*)或雙星號(\*\*)所得到的測試結果皆相同。 ## 補充: 在 IIS 上如果要使用含有 UrlEncode 過的網址參數呼叫 Action 時,接到的參數會少一個 `/`,例如傳 https://linmasaki09.blogspot.com/https%3A%2F%2Fwww.abc.com.tw,則接到的參數值會變為 https://linmasaki09.blogspot.com/https:/www.abc.com.tw,目前的解法是使用 IIS URL Rewrite 模組(沒有需先安裝)在 `web.config` 裡加上以下設定即可解決 ``` Xml
##參考資料 [\[MSDN\] Routing in ASP.NET Core](https://docs.microsoft.com/en-gb/aspnet/core/fundamentals/routing?view=aspnetcore-2.2#differences-from-earlier-versions-of-routing)