C#客户端MultipartFormData经Nginx上传模块转发至ASP.NET异常排查
问题:C# MultipartFormDataContent 与 Nginx Upload Module 兼容问题
我正在尝试通过Nginx Upload Module将C#客户端的图片存储到Nginx服务器,已将Nginx、ASP.NET Core服务、Echo服务部署为Docker容器并组网。使用curl上传文件可正常执行,但C#客户端的请求经Nginx处理后,会向ASP.NET服务转发空请求导致被拒绝。
Nginx配置(基本默认)
load_module modules/ngx_http_upload_module.so; error_log /var/log/nginx/error.log debug; events { } http { server { listen 80; # Allow file uploads max 1024M for example client_max_body_size 1024M; upload_buffer_size 10M; # POST URL location /Upload { # Pass altered request body to this location upload_pass @after_upload; # upload_pass_args on; # Store files to this directory # upload_store /tmp/nginx_upload/; upload_store /upload/; # Allow uploaded files to be world readable upload_store_access user:rw group:rw all:r; # Set specified fields in request body upload_set_form_field upload_file_name "$upload_file_name"; upload_set_form_field upload_content_type "$upload_content_type"; upload_set_form_field upload_tmp_path "$upload_tmp_path"; # Inform backend about hash and size of a file upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5"; upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size"; upload_pass_form_field "^submit$|^description$"; upload_cleanup 400 404 499 500-505; } location @after_upload { # proxy_pass http://commoditytracker; proxy_pass http://echo:8080; # add_header Content-Type "text/plain;charset=utf-8"; # return 200 "Upload succeeded"; } root /upload; location / { root /upload; } location /upload { alias /upload/; } } }
C#客户端代码
构建Form Data(测试图片示例):
Console.WriteLine("Hello, World!"); string path = @"C:\Users\User\Downloads\test.jpg"; byte[] bytes = new byte[8] { 0x40, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }; if (File.Exists(path)) { bytes = File.ReadAllBytes(path); } var fileName = "test.jpg"; var contentType = "image/jpeg"; using (var client = new HttpClient()) { using (var content = new MultipartFormDataContent("boundary1234")) { content.Add(new ByteArrayContent(bytes), "file", $"{fileName}"); List<KeyValuePair<string, string>> b = new List<KeyValuePair<string, string>>(); var requestParameters = new KeyValuePair<string, string>("basketId", Guid.Empty.ToString()); b.Add(requestParameters); var addMe = new FormUrlEncodedContent(b); content.Add(addMe); var Message = await client.PostAsync($"http://xxxxxxxxxxxxxx:8081/upload", content); Console.WriteLine(Message.StatusCode); Console.WriteLine(await Message.Content.ReadAsStringAsync()); } Console.WriteLine("-------------------------------------------"); using (var content = new MultipartFormDataContent("boundary1234")) { content.Add(new ByteArrayContent(bytes), "file", $"{fileName}"); List<KeyValuePair<string, string>> b = new List<KeyValuePair<string, string>>(); var requestParameters = new KeyValuePair<string, string>("basketId", Guid.Empty.ToString()); b.Add(requestParameters); var addMe = new FormUrlEncodedContent(b); content.Add(addMe); var Message = await client.PostAsync($"https://xxxxxxxxxxxxxxx/Upload", content); Console.WriteLine(Message.StatusCode); Console.WriteLine(await Message.Content.ReadAsStringAsync()); // NavigationManager.NavigateTo("fetchdata"); } }
请求结果
C#客户端直接发送至Echo服务的原始请求
{ "path": "/upload", "headers": { "host": "xxxxxxxxxxxxxxxx:8081", "content-type": "image/jpeg", "content-length": "962" }, "method": "POST", "body": "--boundary1234\r\nContent-Disposition: form-data; name=file; filename=test.jpg; filename*=utf-8''test.jpg\r\n\r\n[...]\r\n--boundary1234\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Disposition: form-data\r\n\r\nbasketId=00000000-0000-0000-0000-000000000000\r\n--boundary1234--\r\n", "fresh": false, "hostname": "xxxxxxxxxxxxxxxx", "ip": "xxxxxxxxxxxxxxxx", "ips": [], "protocol": "http", "query": {}, "subdomains": [], "xhr": false, "os": { "hostname": "xxxxxxxxxxxxxx" }, "connection": {} }
xxxxxxxxxxxxxxxx - - [16/Jul/2023:10:40:26 +0000] "POST /upload HTTP/1.1" 200 - "-" "-"
Nginx处理后转发至Echo服务的代理请求
{ "path": "/Upload", "headers": { "host": "echo:8080", "connection": "close", "content-length": "108", "x-real-ip": "10.0.0.2", "x-forwarded-for": "10.0.0.2", "x-forwarded-proto": "https", "x-forwarded-ssl": "on", "x-forwarded-port": "443", "x-original-uri": "/Upload", "content-type": "multipart/form-data; boundary=\"boundary1234\"", "content-disposition": "attachment; filename=test.jpg" }, "method": "POST", "body": "--\"boundary1234\"\r\nContent-Disposition: form-data; name=\"<ngx_upload_module_dummy>\"\r\n\r\n\r\n--\"boundary1234\"--\r\n", "fresh": false, "hostname": "echo", "ip": "10.0.0.2", "ips": [ "10.0.0.2" ], "protocol": "https", "query": {}, "subdomains": [], "xhr": false, "os": { "hostname": "xxxxxxxxxxxxxxxx" }, "connection": {} }
10.0.0.2 - - [16/Jul/2023:10:40:27 +0000] "POST /Upload HTTP/1.0" 200 - "-" "-"
Curl请求对比结果
Curl直接发送至Echo服务的原始请求
PS C:\Users\User\Downloads> curl -X 'POST' 'http://xxxxxxxxxxxxx:8081/Upload?basketId=05aa7960-3f4a-42f4-8112-c22b5f96a9b5' -H 'accept: multipart/form-data' -F "upload_file_name=@$(pwd)/test.jpg"
{ "path": "/Upload", "headers": { "host": "xxxxxxxxxxxxx:8081", "user-agent": "curl/8.0.1", "accept": "multipart/form-data", "content-length": "877", "content-type": "multipart/form-data; boundary=------------------------46a959572bae9574" }, "method": "POST", "body": "--------------------------46a959572bae9574\r\nContent-Disposition: form-data; name=\"upload_file_name\"; filename=\"test.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n[...]\r\n--------------------------46a959572bae9574--\r\n", "fresh": false, "hostname": "xxxxxxxxxxxxxxxxx", "ip": "xxxxxxxxxxxxxxxxxxxxxxxxx", "ips": [], "protocol": "http", "query": { "basketId": "05aa7960-3f4a-42f4-8112-c22b5f96a9b5" }, "subdomains": [], "xhr": false, "os": { "hostname": "xxxxxxxxxxxxxxxxxxx" }, "connection": {} }
xxxxxxxxxxxxxxxxxxxxx - - [16/Jul/2023:11:10:05 +0000] "POST /Upload?basketId=05aa7960-3f4a-42f4-8112-c22b5f96a9b5 HTTP/1.1" 200 - "-" "curl/8.0.1"
Nginx处理后转发至Echo服务的代理请求
PS C:\Users\User\Downloads> curl -X 'POST' 'https://xxxxxxxxxxxxxxx/Upload?basketId=05aa7960-3f4a-42f4-8112-c22b5f96a9b5' -H 'accept: multipart/form-data' -F "upload_file_name=@$(pwd)/test.jpg"
10.0.0.2 - - [16/Jul/2023:11:07:05 +0000] "POST /Upload HTTP/1.0" 200 - "-" "-"
{ "path": "/Upload", "headers": { "host": "echo:8080", "connection": "close", "content-length": "653", "x-real-ip": "10.0.0.2", "x-forwarded-for": "10.0.0.2", "x-forwarded-proto": "https", "x-forwarded-ssl": "on", "x-forwarded-port": "443", "x-original-uri": "/Upload?basketId=05aa7960-3f4a-42f4-8112-c22b5f96a9b5", "user-agent": "curl/8.0.1", "accept": "multipart/form-data", "content-type": "multipart/form-data; boundary=------------------------069346ec29b44fc6" }, "method": "POST", "body": "--------------------------069346ec29b44fc6\r\nContent-Disposition: form-data; name=\"upload_file_name\"\r\n\r\ntest.jpg\r\n--------------------------069346ec29b44fc6\r\nContent-Disposition: form-data; name=\"upload_content_type\"\r\n\r\nimage/jpeg\r\n--------------------------069346ec29b44fc6\r\nContent-Disposition: form-data; name=\"upload_tmp_path\"\r\n\r\n/upload/0002424835\r\n--------------------------069346ec29b44fc6\r\nContent-Disposition: form-data; name=\"upload_file_name.md5\"\r\n\r\n77eb043e0f79321adf635dc9d9895b00\r\n--------------------------069346ec29b44fc6\r\nContent-Disposition: form-data; name=\"upload_file_name.size\"\r\n\r\n679\r\n--------------------------069346ec29b44fc6--\r\n", "fresh": false, "hostname": "echo", "ip": "10.0.0.2", "ips": [ "10.0.0.2" ], "protocol": "https", "query": { "basketId": "05aa7960-3f4a-42f4-8112-c22b5f96a9b5" }, "subdomains": [], "xhr": false, "os": { "hostname": "xxxxxxxxxxxxxxxxxx" }, "connection": {} }
10.0.0.2 - - [16/Jul/2023:11:08:37 +0000] "POST /Upload?basketId=05aa7960-3f4a-42f4-8112-c22b5f96a9b5 HTTP/1.0" 200 - "-" "curl/8.0.1"
我猜测是C#客户端生成的MultipartFormData不符合规范,导致Nginx无法正确处理,但无法确定具体原因。已尝试手动设置ContentType、转义文件名、自定义边界等操作均无效,目前采用手动拼接FormData的临时方案可行,但仍想明确MultipartFormDataContent与Nginx-upload不兼容的根本原因。
内容的提问来源于stack exchange,提问作者Chahed
相关产品推荐
相关产品推荐

