上傳/下載檔案時,若檔名包含 `#` (如 `report#2024.pdf`),使用 `new Uri(url)` 會導致 `#` 後內容被視為 **Fragment (錨點)** 而截斷,造成檔名錯誤、下載失敗。
Uri uri = new Uri("https://server/files/report#2024.pdf");
// uri.AbsoluteUri -> "https://server/files/report"
// uri.Fragment -> "#2024.pdf" (被當作錨點)
// uri.LocalPath -> "/files/report" (遺失 # 後內容)
`Uri` 類別依照 RFC 3986 解析,`#` 為片段識別碼分隔符,**不可直接出現在路徑中**,必須經過 **Percent-Encoding (URL 編碼)** 轉為 `%23`。
public static Uri BuildSafeUri(string baseUrl, string fileName)
{
var builder = new UriBuilder(baseUrl);
// Path 需為絕對路徑,且檔名已編碼
builder.Path = Path.Combine(builder.Path, Uri.EscapeDataString(fileName)).Replace('\\', '/');
// Fragment 保留空白,避免殘留
builder.Fragment = "";
return builder.Uri;
}
// 使用
string baseUrl = "https://server/files/";
string fileName = "report#2024.pdf"; // 含特殊字元
Uri safeUri = BuildSafeUri(baseUrl, fileName);
// 產生:https://server/files/report%232024.pdf
// 編碼檔名 (僅編碼路徑段落,不編碼 / : ? # 等保留字元)
string encodedName = Uri.EscapeDataString(fileName); // # -> %23, 空白 -> %20
// 或
string encodedName = HttpUtility.UrlEncode(fileName, Encoding.UTF8); // 空白 -> + (需注意)
string fullUrl = baseUrl.TrimEnd('/') + "/" + encodedName;
Uri uri = new Uri(fullUrl);
public static Uri FixUriFragmentIssue(Uri originalUri)
{
// 若原始字串含 # 且未編碼,Fragment 會被錯誤解析
if (!originalUri.OriginalString.Contains("#")) return originalUri;
var builder = new UriBuilder
{
Scheme = originalUri.Scheme,
Host = originalUri.Host,
Port = originalUri.Port,
// 關鍵:將 LocalPath + Fragment 合併為新 Path,並清空 Fragment
Path = originalUri.LocalPath + originalUri.Fragment,
Fragment = ""
};
return builder.Uri;
}
// 使用
Uri problematic = new Uri("https://server/files/report#2024.pdf");
Uri fixedUri = FixUriFragmentIssue(problematic);
// fixedUri -> https://server/files/report#2024.pdf (但實際傳輸時會自動編碼為 %23)
// 注意:此法僅修正 Uri 物件內部表示,實際發送請求前仍需確保 Path 正確編碼
public static void UploadFile(string serverUrl, string localFilePath, string fileName)
{
// 1. 建構安全 URI
var builder = new UriBuilder(serverUrl.TrimEnd('/'));
builder.Path = Path.Combine(builder.Path, Uri.EscapeDataString(fileName)).Replace('\\', '/');
Uri requestUri = builder.Uri;
// 2. 建立請求
var request = (HttpWebRequest)WebRequest.Create(requestUri);
request.Method = "PUT"; // 或 POST
request.Credentials = CredentialCache.DefaultCredentials;
// 3. 讀取檔案並寫入請求流
byte[] fileBytes = File.ReadAllBytes(localFilePath);
request.ContentLength = fileBytes.Length;
using (var reqStream = request.GetRequestStream())
{
reqStream.Write(fileBytes, 0, fileBytes.Length);
}
// 4. 取得回應
using (var response = (HttpWebResponse)request.GetResponse())
{
if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created)
throw new Exception($"上傳失敗:{response.StatusCode} {response.StatusDescription}");
}
}
public static void DownloadFile(HttpResponse response, string serverFileUrl, string saveFileName)
{
// 1. 正確編碼 URL
var builder = new UriBuilder(serverFileUrl);
// 假設 serverFileUrl 已包含正確編碼的路徑
Uri requestUri = builder.Uri;
// 2. 請求遠端檔案
var request = (HttpWebRequest)WebRequest.Create(requestUri);
request.Credentials = CredentialCache.DefaultCredentials;
using (var remoteResponse = (HttpWebResponse)request.GetResponse())
using (var remoteStream = remoteResponse.GetResponseStream())
{
// 3. 設定回應標頭 (關鍵:Content-Disposition 檔名編碼)
response.Clear();
response.Buffer = false;
response.ContentType = remoteResponse.ContentType ?? "application/octet-stream";
// RFC 5987 編碼:支援中文檔名
string encodedFileName = HttpUtility.UrlEncode(saveFileName, Encoding.UTF8).Replace("+", "%20");
response.AddHeader("Content-Disposition",
$"attachment; filename=\"{saveFileName}\"; filename*=UTF-8''{encodedFileName}");
response.AddHeader("Content-Length", remoteResponse.ContentLength.ToString());
response.AddHeader("Accept-Ranges", "bytes");
// 4. 分塊傳輸
const int bufferSize = 1024 * 64;
byte[] buffer = new byte[bufferSize];
int read;
while ((read = remoteStream.Read(buffer, 0, bufferSize)) > 0 && response.IsClientConnected)
{
response.OutputStream.Write(buffer, 0, read);
response.Flush();
}
}
response.End();
}
| 字元 | URI 編碼 | 說明 |
|---|---|---|
| # (井號) | %23 | 片段識別碼分隔符,**路徑中必須編碼** |
| ? (問號) | %3F | 查詢字串分隔符,路徑中需編碼 |
| 空白 | %20 (或 +) | 路徑建議 %20,查詢字串可用 + |
| % (百分號) | %25 | 編碼指示符,自身必須編碼 |
| & (アンド) | %26 | 查詢參數分隔符 |
| = (等號) | %3D | 查詢鍵值分隔符 |
| 中文/Unicode | %E4%B8%AD%E6%96%87 | UTF-8 位元組序列各自編碼 |
檔名包含 `#` 等保留字元導致 URI 解析錯誤,是經典陷阱。掌握 `UriBuilder`、`Uri.EscapeDataString`、RFC 5987 `Content-Disposition` 編碼三大工具,即可徹底解決上傳/下載/連結產生中的特殊字元問題,確保跨平台、跨瀏覽器相容性。