diff --git a/src/Core/Extensions/Http.cs b/src/Core/Extensions/Http.cs index a9d35c6..3aade0e 100644 --- a/src/Core/Extensions/Http.cs +++ b/src/Core/Extensions/Http.cs @@ -85,7 +85,7 @@ private static HttpRequestMessage CreateProxiedHttpRequest(this HttpContext cont if (request.HasFormContentType) { usesStreamContent = false; - requestMessage.Content = request.Form.ToHttpContent(request.ContentType); + requestMessage.Content = request.Form.ToHttpContent(request); } else { diff --git a/src/Core/Helpers/Helpers.cs b/src/Core/Helpers/Helpers.cs index 64e35af..1139658 100644 --- a/src/Core/Helpers/Helpers.cs +++ b/src/Core/Helpers/Helpers.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Text; using System.Threading.Tasks; using System.Web; using Microsoft.AspNetCore.Http; @@ -72,7 +73,7 @@ internal static string TrimTrailingSlashes(this string s) return s.Substring(0, s.Length - count); } - internal static HttpContent ToHttpContent(this IFormCollection collection, string contentTypeHeader) + internal static HttpContent ToHttpContent(this IFormCollection collection, HttpRequest request) { // @PreferLinux: // Form content types resource: https://stackoverflow.com/questions/4526273/what-does-enctype-multipart-form-data-mean/28380690 @@ -84,7 +85,7 @@ internal static HttpContent ToHttpContent(this IFormCollection collection, strin // A single form element can have multiple values. When sending them they are handled as separate items with the same name, not a singe item with multiple values. // For example, a=1&a=2. - var contentType = MediaTypeHeaderValue.Parse(contentTypeHeader); + var contentType = MediaTypeHeaderValue.Parse(request.ContentType); if (contentType.MediaType.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) // specification: https://url.spec.whatwg.org/#concept-urlencoded return new FormUrlEncodedContent(collection.SelectMany(formItemList => formItemList.Value.Select(value => new KeyValuePair(formItemList.Key, value)))); @@ -106,9 +107,15 @@ internal static HttpContent ToHttpContent(this IFormCollection collection, strin foreach (var file in collection.Files) { var content = new StreamContent(file.OpenReadStream()); - foreach (var header in file.Headers) + foreach (var header in file.Headers.Where(h => !h.Key.Equals("Content-Disposition", StringComparison.OrdinalIgnoreCase))) content.Headers.TryAddWithoutValidation(header.Key, (IEnumerable)header.Value); - multipart.Add(content, file.Name, file.FileName); + + // Force content-disposition header to use raw string to ensure UTF-8 is well encoded. + content.Headers.TryAddWithoutValidation("Content-Disposition", + new string(Encoding.UTF8.GetBytes($"form-data; name=\"{file.Name}\"; filename=\"{file.FileName}\""). + Select(b => (char)b).ToArray())); + + multipart.Add(content); } return multipart; } diff --git a/src/Test/Http/HttpIntegrationTests.cs b/src/Test/Http/HttpIntegrationTests.cs index 6adbe42..6894779 100644 --- a/src/Test/Http/HttpIntegrationTests.cs +++ b/src/Test/Http/HttpIntegrationTests.cs @@ -79,7 +79,7 @@ public async Task CanProxyControllerPostWithFormAndFilesRequest() const string fileString = "This is a test file こんにちは with non-ascii content."; var fileContent = new StreamContent(new System.IO.MemoryStream(Encoding.UTF8.GetBytes(fileString))); content.Add(fileContent, "testFile", fileName); - + var response = await _client.PostAsync("api/multipart", content); response.EnsureSuccessStatusCode(); var responseString = await response.Content.ReadAsStringAsync(); diff --git a/src/Test/Unit/Other.cs b/src/Test/Unit/Other.cs index b8d5fce..9bd274a 100644 --- a/src/Test/Unit/Other.cs +++ b/src/Test/Unit/Other.cs @@ -1,5 +1,7 @@ using System; +using Microsoft.AspNetCore.Http; +using Moq; using Xunit; namespace AspNetCore.Proxy.Tests @@ -10,9 +12,11 @@ public class Other public void CanFailOnBadFormContentType() { var contentType = "text/plain"; + var request = Mock.Of(); + request.ContentType = contentType; var e = Assert.ThrowsAny(() => { - var dummy = Helpers.ToHttpContent(null, contentType); + var dummy = Helpers.ToHttpContent(null, request); }); Assert.Equal($"Unknown form content type `{contentType}`.", e.Message);