10

A year ago I wrote a cmdlet that handles Multipart/form-data requests and it is leveraging the .net class HttpClient in order to do so. I described it in details here.

In a nutshell this is the core of my cmdlet:

$networkCredential = New-Object -TypeName System.Net.NetworkCredential -ArgumentList @($Credential.UserName, $Credential.Password)
$httpClientHandler = New-Object -TypeName System.Net.Http.HttpClientHandler
$httpClientHandler.Credentials = $networkCredential

$httpClient = New-Object -TypeName System.Net.Http.Httpclient -ArgumentList @($httpClientHandler)

$packageFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList @($packagePath, [System.IO.FileMode]::Open)

$contentDispositionHeaderValue = New-Object -TypeName  System.Net.Http.Headers.ContentDispositionHeaderValue -ArgumentList @("form-data")
$contentDispositionHeaderValue.Name = "fileData"
$contentDispositionHeaderValue.FileName = $fileName

$streamContent = New-Object -TypeName System.Net.Http.StreamContent -ArgumentList @($packageFileStream)
$streamContent.Headers.ContentDisposition = $contentDispositionHeaderValue
$streamContent.Headers.ContentType = New-Object -TypeName System.Net.Http.Headers.MediaTypeHeaderValue -ArgumentList @("application/octet-stream")

$content = New-Object -TypeName System.Net.Http.MultipartFormDataContent
$content.Add($streamContent)

try
{
    $response = $httpClient.PostAsync("$EndpointUrl/package/upload/$fileName", $content).GetAwaiter().GetResult()

    if (!$response.IsSuccessStatusCode)
    {
        $responseBody = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
        $errorMessage = "Status code {0}. Reason {1}. Server reported the following message: {2}." -f $response.StatusCode, $response.ReasonPhrase, $responseBody

        throw [System.Net.Http.HttpRequestException] $errorMessage
    }

    return [xml]$response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
}
catch [Exception]
{
    throw
}
finally
{
    if($null -ne $httpClient)
    {
        $httpClient.Dispose()
    }

    if($null -ne $response)
    {
        $response.Dispose()
    }
}

I was using this code in a VSTS Build task for over a year with success. Recently it started intermittently failing. In one run it succeeds then the next one fails and so on. I can't understand why this is the case and I would use some help.

The code fails on PostAsync method invocation and following is the exception I do see:

2017-03-24T15:17:38.4470248Z ##[debug]System.NotSupportedException: The stream does not support concurrent IO read or write operations.
2017-03-24T15:17:38.4626512Z ##[debug]   at System.Net.ConnectStream.InternalWrite(Boolean async, Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)
2017-03-24T15:17:38.4626512Z ##[debug]   at System.Net.ConnectStream.BeginWrite(Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)
2017-03-24T15:17:38.4626512Z ##[debug]   at System.Net.Http.StreamToStreamCopy.TryStartWriteSync(Int32 bytesRead)
2017-03-24T15:17:38.4626512Z ##[debug]   at System.Net.Http.StreamToStreamCopy.BufferReadCallback(IAsyncResult ar)
2017-03-24T15:17:38.4626512Z ##[debug]--- End of stack trace from previous location where exception was thrown ---
2017-03-24T15:17:38.4626512Z ##[debug]   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
2017-03-24T15:17:38.4626512Z ##[debug]   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
2017-03-24T15:17:38.4626512Z ##[debug]   at CallSite.Target(Closure , CallSite , Object )
2017-03-24T15:17:38.4939015Z ##[error]The stream does not support concurrent IO read or write operations.

I have tried to execute this on different build servers, on hosted agent, etc. but the result is the same. Also if I try to execute this code from an interactive session on my build server from ISE I can't have the code to fail. I posed a similar question on the VSTS build task project here but till now, no luck.

Does anyone have a suggestion or a test I can do in order to understand what is going on and why is this failing?

I would appreciate any tip.

UPDATE 1:

I executed my code by running it as a background job via a Start-Job cmdlet and I can't get it to fail. So it needs to be something related with the way the VSTS agent executes my code. I'll keep digging. If you have any suggestions, they are still more then welcome.

iehrlich
  • 3,572
  • 4
  • 34
  • 43
Mario Majcica
  • 780
  • 1
  • 6
  • 20

3 Answers3

3

System.Net.Http.StreamContent and System.IO.FileStream are instantiated but not disposed.

Add the code to invoke Dispose like you did in finally block.

try
{
    # enclose the program
}
finally
{
    if($null -ne $streamContent)
    {
        $streamContent.Dispose()
    }
    if($null -ne $packageFileStream)
    {
        $packageFileStream.Dispose()
    }
}
cshu
  • 5,654
  • 28
  • 44
  • I'm not explicitly disposing those streams as MultipartFormDataContent base class is taking care of it meanwhile disposing itself, which again is handled by the HttpClient itself when calling DisposeRequestContent from the SendAsync method. But I will check this again just to be 100% sure. – Mario Majcica Mar 30 '17 at 12:53
  • @MarioMajcica Just interested - how do you know that MultipartFormDataContent is disposing of its content? I can't see it mentioned in the [docs](https://msdn.microsoft.com/en-us/library/system.net.http.multipartformdatacontent(v=vs.118).aspx). – Dunc Sep 08 '17 at 13:12
  • 1
    @Dunc I decompiled the code with dotPeak and checked what is happening. – Mario Majcica Sep 11 '17 at 07:50
1

That exception is clearly threading-related.

Your method calls use async method calls, but not correctly:

.ReadAsStringAsync().GetAwaiter().GetResult()

just blocks the current thread while waiting for another: it actually takes up an additional thread.

So, because you're a) not using threading in a useful fashion and b) getting an exception for your efforts, I suggest you take anywhere in your code which uses that construct and replace it with a normal method call.

Edit:

Threading issues aside, you could try

$content.Add($streamContent)
$content.LoadIntoBufferAsync().GetAwaiter().GetResult()

to force the content stream to load prior to the next async post operation.

Nathan
  • 6,095
  • 2
  • 35
  • 61
  • Calling this methods instead of just accessing the .Result property is because it is the only way to have my exception popped out. What is the normal method call you are suggesting? – Mario Majcica Apr 05 '17 at 08:46
  • http://stackoverflow.com/questions/8222092/sending-http-post-with-system-net-webclient – Nathan Apr 05 '17 at 09:19
  • httpclient is "recommended" because it scales better [when used with correct async calls], but webclient (or WebRequest depending on your needs) is perfectly fine for your scenario (and doesn't complicate matters). – Nathan Apr 05 '17 at 09:21
  • just suggested something in case you don't have time to go with the fully synchronous option. – Nathan Apr 05 '17 at 09:32
  • That is not a great suggestion. I can't replicate in the code manually all of the necessary for this type of request, just to overcome an async call. It is all encapsulated nicely inside the classes provided for HttpClient. Also I need to use streams and can load into buffer the content of that stream because of it size, often a GB or more. – Mario Majcica Apr 05 '17 at 12:15
  • Indeed, it's not ideal - but neither is shoe-horning an async API into powershell... your mileage may vary. – Nathan Apr 05 '17 at 13:23
  • The last thing that pops into my mind is the possibility of writing a helper/wrapper class which enforces thread-safety on the objects you're working with. – Nathan Apr 05 '17 at 13:25
  • This is actually my current workaround. I wrote another cmdlet that executes the incriminated one as a background job via a Start-Job cmdlet. This works well. However, I'm sure it has to the with the newer version of VSTS build agent and the way it executes tasks. Unfortunately I'm unable to pinpoint how and why. – Mario Majcica Apr 05 '17 at 19:42
0

Be careful - this NotSupportedException can mask a different error, such as an authorization problem (e.g. incorrect credentials passed in the NetworkCredential object).

I found this out by running Fiddler and watching the responses.

Dunc
  • 18,404
  • 6
  • 86
  • 103