diff --git a/Test/Flurl.Test/Http/MultipartTests.cs b/Test/Flurl.Test/Http/MultipartTests.cs index 91419f8d..53083ec4 100644 --- a/Test/Flurl.Test/Http/MultipartTests.cs +++ b/Test/Flurl.Test/Http/MultipartTests.cs @@ -17,29 +17,33 @@ public class MultipartTests public void can_build_multipart_content() { var content = new CapturedMultipartContent() .AddString("string", "foo") + .AddString("string2", "bar", "text/blah") .AddStringParts(new { part1 = 1, part2 = 2, part3 = (string)null }) // part3 should be excluded .AddFile("file1", Path.Combine("path", "to", "image1.jpg"), "image/jpeg") .AddFile("file2", Path.Combine("path", "to", "image2.jpg"), "image/jpeg", fileName: "new-name.jpg") .AddJson("json", new { foo = "bar" }) .AddUrlEncoded("urlEnc", new { fizz = "buzz" }); - Assert.AreEqual(7, content.Parts.Length); - - AssertStringPart(content.Parts[0], "string", "foo"); - AssertStringPart(content.Parts[1], "part1", "1"); - AssertStringPart(content.Parts[2], "part2", "2"); - AssertFilePart(content.Parts[3], "file1", "image1.jpg", "image/jpeg"); - AssertFilePart(content.Parts[4], "file2", "new-name.jpg", "image/jpeg"); - AssertStringPart(content.Parts[5], "json", "{\"foo\":\"bar\"}"); - AssertStringPart(content.Parts[6], "urlEnc", "fizz=buzz"); + Assert.AreEqual(8, content.Parts.Length); + AssertStringPart(content.Parts[0], "string", "foo", null); + AssertStringPart(content.Parts[1], "string2", "bar", "text/blah"); + AssertStringPart(content.Parts[2], "part1", "1", null); + AssertStringPart(content.Parts[3], "part2", "2", null); + AssertFilePart(content.Parts[4], "file1", "image1.jpg", "image/jpeg"); + AssertFilePart(content.Parts[5], "file2", "new-name.jpg", "image/jpeg"); + AssertStringPart(content.Parts[6], "json", "{\"foo\":\"bar\"}", "application/json; charset=UTF-8"); + AssertStringPart(content.Parts[7], "urlEnc", "fizz=buzz", "application/x-www-form-urlencoded"); } - private void AssertStringPart(HttpContent part, string name, string content) { + private void AssertStringPart(HttpContent part, string name, string content, string contentType) { Assert.IsInstanceOf(part); Assert.AreEqual(name, part.Headers.ContentDisposition.Name); Assert.AreEqual(content, (part as CapturedStringContent)?.Content); - Assert.IsFalse(part.Headers.Contains("Content-Type")); // #392 - } + if (contentType == null) + Assert.IsFalse(part.Headers.Contains("Content-Type")); // #392 + else + Assert.AreEqual(contentType, part.Headers.GetValues("Content-Type").SingleOrDefault()); + } private void AssertFilePart(HttpContent part, string name, string fileName, string contentType) { Assert.IsInstanceOf(part); diff --git a/src/Flurl.Http/Content/CapturedJsonContent.cs b/src/Flurl.Http/Content/CapturedJsonContent.cs index 1567cbd9..7b2ccd6b 100644 --- a/src/Flurl.Http/Content/CapturedJsonContent.cs +++ b/src/Flurl.Http/Content/CapturedJsonContent.cs @@ -12,6 +12,6 @@ public class CapturedJsonContent : CapturedStringContent /// Initializes a new instance of the class. /// /// The json. - public CapturedJsonContent(string json) : base(json, Encoding.UTF8, "application/json") { } + public CapturedJsonContent(string json) : base(json, "application/json; charset=UTF-8") { } } } \ No newline at end of file diff --git a/src/Flurl.Http/Content/CapturedMultipartContent.cs b/src/Flurl.Http/Content/CapturedMultipartContent.cs index 7cd4ca5d..79117beb 100644 --- a/src/Flurl.Http/Content/CapturedMultipartContent.cs +++ b/src/Flurl.Http/Content/CapturedMultipartContent.cs @@ -35,31 +35,29 @@ public CapturedMultipartContent(FlurlHttpSettings settings = null) : base("form- /// The control name of the part. /// The HttpContent of the part. /// This CapturedMultipartContent instance (supports method chaining). - public CapturedMultipartContent Add(string name, HttpContent content) => AddInternal(name, content, true, null); + public CapturedMultipartContent Add(string name, HttpContent content) => AddInternal(name, content, null); /// /// Add a simple string part to the multipart request. /// - /// The control name of the part. - /// The string content of the part. - /// The encoding of the part. - /// The media type of the part. + /// The name of the part. + /// The string value of the part. + /// The value of the Content-Type header for this part. If null (the default), header will be excluded, which complies with the HTML 5 standard. /// This CapturedMultipartContent instance (supports method chaining). - public CapturedMultipartContent AddString(string name, string content, Encoding encoding = null, string mediaType = null) => - AddInternal(name, new CapturedStringContent(content, encoding, mediaType), false, null); + public CapturedMultipartContent AddString(string name, string value, string contentType = null) => + AddInternal(name, new CapturedStringContent(value, contentType), null); /// /// Add multiple string parts to the multipart request by parsing an object's properties into control name/content pairs. /// /// The object (typically anonymous) whose properties are parsed into control name/content pairs. - /// The encoding of the parts. - /// The media type of the parts. + /// The value of the Content-Type header for this part. If null, header will be excluded, which complies with the HTML 5 standard. /// This CapturedMultipartContent instance (supports method chaining). - public CapturedMultipartContent AddStringParts(object data, Encoding encoding = null, string mediaType = null) { + public CapturedMultipartContent AddStringParts(object data, string contentType = null) { foreach (var kv in data.ToKeyValuePairs()) { if (kv.Value == null) continue; - AddString(kv.Key, kv.Value.ToInvariantString(), encoding, mediaType); + AddString(kv.Key, kv.Value.ToInvariantString(), contentType); } return this; } @@ -71,7 +69,7 @@ public CapturedMultipartContent AddStringParts(object data, Encoding encoding = /// The content of the part, which will be serialized to JSON. /// This CapturedMultipartContent instance (supports method chaining). public CapturedMultipartContent AddJson(string name, object data) => - AddInternal(name, new CapturedJsonContent(_settings.JsonSerializer.Serialize(data)), false, null); + AddInternal(name, new CapturedJsonContent(_settings.JsonSerializer.Serialize(data)), null); /// /// Add a URL-encoded part to the multipart request. @@ -80,7 +78,7 @@ public CapturedMultipartContent AddJson(string name, object data) => /// The content of the part, whose properties will be parsed and serialized to URL-encoded format. /// This CapturedMultipartContent instance (supports method chaining). public CapturedMultipartContent AddUrlEncoded(string name, object data) => - AddInternal(name, new CapturedUrlEncodedContent(_settings.UrlEncodedSerializer.Serialize(data)), false, null); + AddInternal(name, new CapturedUrlEncodedContent(_settings.UrlEncodedSerializer.Serialize(data)), null); /// /// Adds a file to the multipart request from a stream. @@ -88,14 +86,14 @@ public CapturedMultipartContent AddUrlEncoded(string name, object data) => /// The control name of the part. /// The file stream to send. /// The filename, added to the Content-Disposition header of the part. - /// The media type of the file. + /// The content type of the file. /// The buffer size of the stream upload in bytes. Defaults to 4096. /// This CapturedMultipartContent instance (supports method chaining). - public CapturedMultipartContent AddFile(string name, Stream stream, string fileName, string mediaType = null, int bufferSize = 4096) { + public CapturedMultipartContent AddFile(string name, Stream stream, string fileName, string contentType = null, int bufferSize = 4096) { var content = new StreamContent(stream, bufferSize); - if (mediaType != null) - content.Headers.ContentType = new MediaTypeHeaderValue(mediaType); - return AddInternal(name, content, true, fileName); + if (contentType != null) + content.Headers.TryAddWithoutValidation("Content-Type", contentType); + return AddInternal(name, content, fileName); } /// @@ -103,27 +101,22 @@ public CapturedMultipartContent AddFile(string name, Stream stream, string fileN /// /// The control name of the part. /// The local path to the file. - /// The media type of the file. + /// The content type of the file. /// The buffer size of the stream upload in bytes. Defaults to 4096. /// The filename, added to the Content-Disposition header of the part. Defaults to local file name. /// This CapturedMultipartContent instance (supports method chaining). - public CapturedMultipartContent AddFile(string name, string path, string mediaType = null, int bufferSize = 4096, string fileName = null) { + public CapturedMultipartContent AddFile(string name, string path, string contentType = null, int bufferSize = 4096, string fileName = null) { fileName = fileName ?? FileUtil.GetFileName(path); var content = new FileContent(path, bufferSize); - if (mediaType != null) - content.Headers.ContentType = new MediaTypeHeaderValue(mediaType); - return AddInternal(name, content, true, fileName); + if (contentType != null) + content.Headers.TryAddWithoutValidation("Content-Type", contentType); + return AddInternal(name, content, fileName); } - private CapturedMultipartContent AddInternal(string name, HttpContent content, bool allowContentType, string fileName) { + private CapturedMultipartContent AddInternal(string name, HttpContent content, string fileName) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name must not be empty", nameof(name)); - // StringContent is the simplest way to add a string part, but it always - // includes Content-Type, and per HTML spec that's not allowed (#392) - if (!allowContentType && content.Headers.Contains("Content-Type")) - content.Headers.Remove("Content-Type"); - content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = name, FileName = fileName, diff --git a/src/Flurl.Http/Content/CapturedStringContent.cs b/src/Flurl.Http/Content/CapturedStringContent.cs index b2eeef8c..598b0830 100644 --- a/src/Flurl.Http/Content/CapturedStringContent.cs +++ b/src/Flurl.Http/Content/CapturedStringContent.cs @@ -1,4 +1,5 @@ using System.Net.Http; +using System.Net.Http.Headers; using System.Text; namespace Flurl.Http.Content @@ -14,16 +15,24 @@ public class CapturedStringContent : StringContent /// public string Content { get; } + /// + /// Initializes a new instance of with a Content-Type header of text/plain; charset=UTF-8 + /// + /// The content. + public CapturedStringContent(string content) : base(content) { + Content = content; + } + /// /// Initializes a new instance of the class. /// /// The content. - /// The encoding. - /// Type of the media. - public CapturedStringContent(string content, Encoding encoding = null, string mediaType = null) : - base(content, encoding, mediaType) - { + /// Value of the Content-Type header. To exclude the header, set to null explicitly. + public CapturedStringContent(string content, string contentType) : base(content) { Content = content; + Headers.Remove("Content-Type"); + if (contentType != null) + Headers.TryAddWithoutValidation("Content-Type", contentType); } } } \ No newline at end of file diff --git a/src/Flurl.Http/Content/CapturedUrlEncodedContent.cs b/src/Flurl.Http/Content/CapturedUrlEncodedContent.cs index fb86042d..b9add7c3 100644 --- a/src/Flurl.Http/Content/CapturedUrlEncodedContent.cs +++ b/src/Flurl.Http/Content/CapturedUrlEncodedContent.cs @@ -11,6 +11,6 @@ public class CapturedUrlEncodedContent : CapturedStringContent /// Initializes a new instance of the class. /// /// Content represented as a (typically anonymous) object, which will be parsed into name/value pairs. - public CapturedUrlEncodedContent(string data) : base(data, null, "application/x-www-form-urlencoded") { } + public CapturedUrlEncodedContent(string data) : base(data, "application/x-www-form-urlencoded") { } } } \ No newline at end of file diff --git a/src/Flurl.Http/HttpMessageExtensions.cs b/src/Flurl.Http/HttpMessageExtensions.cs index 53d32393..5826f1ca 100644 --- a/src/Flurl.Http/HttpMessageExtensions.cs +++ b/src/Flurl.Http/HttpMessageExtensions.cs @@ -91,7 +91,6 @@ private static void SetHeader(this HttpMessage msg, string name, object value, b } } - /// /// Wrapper class for treating HttpRequestMessage and HttpResponseMessage uniformly. (Unfortunately they don't have a common interface.) ///