New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Multipart Support to Web Cmdlets #4782
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -359,6 +359,39 @@ function ExecuteRestMethod | |
return $result | ||
} | ||
|
||
function GetMultipartBody | ||
{ | ||
param | ||
( | ||
[Switch]$String, | ||
[Switch]$File | ||
) | ||
$multipartContent = [System.Net.Http.MultipartFormDataContent]::new() | ||
if ($String.IsPresent) | ||
{ | ||
$stringHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") | ||
$stringHeader.Name = "TestString" | ||
$StringContent = [System.Net.Http.StringContent]::new("TestValue") | ||
$StringContent.Headers.ContentDisposition = $stringHeader | ||
$multipartContent.Add($stringContent) | ||
} | ||
if ($File.IsPresent) | ||
{ | ||
$multipartFile = Join-Path $TestDrive 'multipart.txt' | ||
"TestContent" | Set-Content $multipartFile | ||
$FileStream = [System.IO.FileStream]::new($multipartFile, [System.IO.FileMode]::Open) | ||
$fileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") | ||
$fileHeader.Name = "TestFile" | ||
$fileHeader.FileName = 'multipart.txt' | ||
$fileContent = [System.Net.Http.StreamContent]::new($FileStream) | ||
$fileContent.Headers.ContentDisposition = $fileHeader | ||
$fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("text/plain") | ||
$multipartContent.Add($fileContent) | ||
} | ||
# unary comma required to prevent $multipartContent from being unwrapped/enumerated | ||
return ,$multipartContent | ||
} | ||
|
||
<# | ||
Defines the list of redirect codes to test as well as the | ||
expected Method when the redirection is handled. | ||
|
@@ -1200,6 +1233,41 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { | |
} | ||
} | ||
|
||
Context "Multipart/form-data Tests" { | ||
It "Verifies Invoke-WebRequest Supports Multipart String Values" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use the same Context header 😕 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, because they are under different describe blocks. We did the same thing with HTTPS There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's somewhat confusing, but we can leave it for Context. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For clarity, would you like me to change it or leave it as is? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leave it as is. |
||
$body = GetMultipartBody -String | ||
$uri = Get-WebListenerUrl -Test 'Multipart' | ||
$response = Invoke-WebRequest -Uri $uri -Body $body -Method 'POST' | ||
$result = $response.Content | ConvertFrom-Json | ||
|
||
$result.Headers.'Content-Type' | Should Match 'multipart/form-data' | ||
$result.Items.TestString[0] | Should Be 'TestValue' | ||
} | ||
It "Verifies Invoke-WebRequest Supports Multipart File Values" { | ||
$body = GetMultipartBody -File | ||
$uri = Get-WebListenerUrl -Test 'Multipart' | ||
$response = Invoke-WebRequest -Uri $uri -Body $body -Method 'POST' | ||
$result = $response.Content | ConvertFrom-Json | ||
|
||
$result.Headers.'Content-Type' | Should Match 'multipart/form-data' | ||
$result.Files[0].FileName | Should Be 'multipart.txt' | ||
$result.Files[0].ContentType | Should Be 'text/plain' | ||
$result.Files[0].Content | Should Match 'TestContent' | ||
} | ||
It "Verifies Invoke-WebRequest Supports Mixed Multipart String and File Values" { | ||
$body = GetMultipartBody -String -File | ||
$uri = Get-WebListenerUrl -Test 'Multipart' | ||
$response = Invoke-WebRequest -Uri $uri -Body $body -Method 'POST' | ||
$result = $response.Content | ConvertFrom-Json | ||
|
||
$result.Headers.'Content-Type' | Should Match 'multipart/form-data' | ||
$result.Items.TestString[0] | Should Be 'TestValue' | ||
$result.Files[0].FileName | Should Be 'multipart.txt' | ||
$result.Files[0].ContentType | Should Be 'text/plain' | ||
$result.Files[0].Content | Should Match 'TestContent' | ||
} | ||
} | ||
|
||
BeforeEach { | ||
if ($env:http_proxy) { | ||
$savedHttpProxy = $env:http_proxy | ||
|
@@ -1737,6 +1805,38 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" { | |
} | ||
} | ||
|
||
Context "Multipart/form-data Tests" { | ||
It "Verifies Invoke-RestMethod Supports Multipart String Values" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Context "Invoke-RestMethod Multipart/form-data Tests" |
||
$body = GetMultipartBody -String | ||
$uri = Get-WebListenerUrl -Test 'Multipart' | ||
$result = Invoke-RestMethod -Uri $uri -Body $body -Method 'POST' | ||
|
||
$result.Headers.'Content-Type' | Should Match 'multipart/form-data' | ||
$result.Items.TestString[0] | Should Be 'TestValue' | ||
} | ||
It "Verifies Invoke-RestMethod Supports Multipart File Values" { | ||
$body = GetMultipartBody -File | ||
$uri = Get-WebListenerUrl -Test 'Multipart' | ||
$result = Invoke-RestMethod -Uri $uri -Body $body -Method 'POST' | ||
|
||
$result.Headers.'Content-Type' | Should Match 'multipart/form-data' | ||
$result.Files[0].FileName | Should Be 'multipart.txt' | ||
$result.Files[0].ContentType | Should Be 'text/plain' | ||
$result.Files[0].Content | Should Match 'TestContent' | ||
} | ||
It "Verifies Invoke-RestMethod Supports Mixed Multipart String and File Values" { | ||
$body = GetMultipartBody -String -File | ||
$uri = Get-WebListenerUrl -Test 'Multipart' | ||
$result = Invoke-RestMethod -Uri $uri -Body $body -Method 'POST' | ||
|
||
$result.Headers.'Content-Type' | Should Match 'multipart/form-data' | ||
$result.Items.TestString[0] | Should Be 'TestValue' | ||
$result.Files[0].FileName | Should Be 'multipart.txt' | ||
$result.Files[0].ContentType | Should Be 'text/plain' | ||
$result.Files[0].Content | Should Match 'TestContent' | ||
} | ||
} | ||
|
||
#region charset encoding tests | ||
|
||
Context "Invoke-RestMethod Encoding tests with BasicHtmlWebResponseObject response" { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -116,6 +116,7 @@ function Get-WebListenerUrl { | |
'Cert', | ||
'Get', | ||
'Home', | ||
'Multipart', | ||
'/' | ||
)] | ||
[String]$Test | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Hosting; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.Net.Http.Headers; | ||
using mvc.Models; | ||
|
||
|
||
namespace mvc.Controllers | ||
{ | ||
public class MultipartController : Controller | ||
{ | ||
private IHostingEnvironment _environment; | ||
|
||
public MultipartController(IHostingEnvironment environment) | ||
{ | ||
_environment = environment; | ||
} | ||
public ActionResult Index() | ||
{ | ||
return View(); | ||
} | ||
|
||
[HttpPost] | ||
public JsonResult Index(IFormCollection collection) | ||
{ | ||
if (!Request.HasFormContentType) | ||
{ | ||
Response.StatusCode = 415; | ||
Hashtable error = new Hashtable {{"error","Unsupported media type"}}; | ||
return Json(error); | ||
} | ||
|
||
List<Hashtable> fileList = new List<Hashtable>(); | ||
foreach (var file in collection.Files) | ||
{ | ||
string result = string.Empty; | ||
if (file.Length > 0) | ||
{ | ||
using (var reader = new StreamReader(file.OpenReadStream())) | ||
{ | ||
result = reader.ReadToEnd(); | ||
} | ||
} | ||
Hashtable fileHash = new Hashtable | ||
{ | ||
{"ContentDisposition" , file.ContentDisposition}, | ||
{"ContentType" , file.ContentType}, | ||
{"FileName" , file.FileName}, | ||
{"Length" , file.Length}, | ||
{"Name" , file.Name}, | ||
{"Content" , result}, | ||
{"Headers" , file.Headers} | ||
}; | ||
fileList.Add(fileHash); | ||
} | ||
Hashtable itemsHash = new Hashtable(); | ||
foreach (var key in collection.Keys) | ||
{ | ||
itemsHash.Add(key,collection[key]); | ||
} | ||
MediaTypeHeaderValue mediaContentType = MediaTypeHeaderValue.Parse(Request.ContentType); | ||
Hashtable headers = new Hashtable(); | ||
foreach (var key in Request.Headers.Keys) | ||
{ | ||
headers.Add(key, String.Join(Constants.HeaderSeparator, Request.Headers[key])); | ||
} | ||
Hashtable output = new Hashtable | ||
{ | ||
{"Files" , fileList}, | ||
{"Items" , itemsHash}, | ||
{"Boundary", HeaderUtilities.RemoveQuotes(mediaContentType.Boundary).Value}, | ||
{"Headers" , headers} | ||
}; | ||
return Json(output); | ||
} | ||
|
||
public IActionResult Error() | ||
{ | ||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<form name="MultipartForm" method="post" enctype="multipart/form-data"> | ||
<div> | ||
<label for="TextBox">TextBox text field</label> | ||
<input name="TextBox" type="text" /> | ||
</div> | ||
<div> | ||
<label for="TextFile">TextFile file field</label> | ||
<input name="TextFile" type="file" /> | ||
</div> | ||
<div> | ||
<input type="submit" value="Submit" /> | ||
</div> | ||
</form> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WebSession.ContentHeader
is set at line 325 whenContentType
is specified. Is it OK to ignore that as well?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes,
ContentType
must be set by theMultipartFormDataContent
object as it controls theboundary
in theContent-Type
header used to distinguish between the various fields supplied. Documentation will probably need to make clear that-ContentType
will be ignored when aMultipartFormDataContent
object is supplied. It doesn't make sense to use any otherContent-Type
header when doingmultipart/form-data
anyway. If this isn't done here, the Foreach on line 421 throws. whenContent
is set with aMultipartFormDataContent
it overwrites all the content headers, so trying to add content headers again causes an exception aboutContentType
already existing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the clarification. I updated the PR description to capture your words.
Then there is a potential problem at line 239Nevermind,
GetRequest
happens beforeFillRequestStream
, so we are good. Updated the PR description to mention-Header
and-WebSession
will be ignored as well.