56

I have an API for a system written using the ASP.NET Web Api and am trying to extend it to allow images to be uploaded. I have done some googling and found how the recommended way to accept files using MultpartMemoryStreamProvider and some async methods but my await on the ReadAsMultipartAsync never returns.

Here is the code:

[HttpPost]
public async Task<HttpResponseMessage> LowResImage(int id)
{
    if (!Request.Content.IsMimeMultipartContent())
    {
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    }

    var provider = new MultipartMemoryStreamProvider();

    try
    {
        await Request.Content.ReadAsMultipartAsync(provider);

        foreach (var item in provider.Contents)
        {
            if (item.Headers.ContentDisposition.FileName != null)
            {

            }
        }

        return Request.CreateResponse(HttpStatusCode.OK);
    }
    catch (System.Exception e)
    {
        return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
    }
}

I can step through all the way to:

await Request.Content.ReadAsMultipartAsync(provider);

at which point it will never complete.

What is the reason why my await never returns?

Update

I am attempting to POST to this action using curl, the command is as follows:

C:\cURL>curl -i -F filedata=@C:\LowResExample.jpg http://localhost:8000/Api/Photos/89/LowResImage

I have also tried using the following html to POST to the action as well and the same thing happens:

<form method="POST" action="http://localhost:8000/Api/Photos/89/LowResImage" enctype="multipart/form-data">
    <input type="file" name="fileupload"/>
    <input type="submit" name="submit"/>
</form>
Community
  • 1
  • 1
Jon Cahill
  • 4,968
  • 4
  • 34
  • 31
  • How does your client code look like? – svick Mar 04 '13 at 16:29
  • Could you share how your how raw request looks like? – Kiran Mar 04 '13 at 17:31
  • Put a breakpoint in the code after the `await` as well. Sometimes it doesnt break/step to the next line when you're using async/await (from my experience) – Micah Mar 05 '13 at 02:16
  • 1
    hmm...i tried with the same code and the html above, but i am not seeing the issue you are mentioning... – Kiran Mar 05 '13 at 02:58
  • I am from the asp.net web api team. I would be interested to debug more if you can send me your repro project. My email: kiranchalla@hotmail.com – Kiran Mar 05 '13 at 03:00

5 Answers5

101

I ran into something similar in .NET 4.0 (no async/await). Using the debugger's Thread stack I could tell that ReadAsMultipartAsync was launching the task onto the same thread, so it would deadlock. I did something like this:

IEnumerable<HttpContent> parts = null;
Task.Factory
    .StartNew(() => parts = Request.Content.ReadAsMultipartAsync().Result.Contents,
        CancellationToken.None,
        TaskCreationOptions.LongRunning, // guarantees separate thread
        TaskScheduler.Default)
    .Wait();

The TaskCreationOptions.LongRunning parameter was key for me because without it, the call would continue to launch the task onto the same thread. You could try using something like the following pseudocode to see if it works for you in C# 5.0:

await TaskEx.Run(async() => await Request.Content.ReadAsMultipartAsync(provider))
yeejuto
  • 1,174
  • 1
  • 7
  • 7
  • 6
    Excellent work! I met exactly the problem and solved it by this. – guogangj May 22 '13 at 08:32
  • I'm working with Web API and I needed to call an async method inside a constructor. This was the only way I managed to make it work! Thanks! – Lucio Paiva Jul 23 '13 at 15:19
  • 10
    Seriously a million thanks for sharing this. Does anyone know why this is the default behavior if it results in a block? – leojh Aug 26 '13 at 16:19
  • You are a hero! I was looking for this whole day. I was developing a custom Model Binder, where I simply cannot return a Task - so I had to do it synchronously. This is typical MS again: only the MSDN example works - no other scenarios are imagined. – Daniel Leiszen Apr 18 '14 at 10:39
  • 3
    You have saved me. I've wasted about 3 days on this. One small thing though, if one is using a custom MultipartForDataStreamProvider you can actually change the internal of `StartNew` Action to read something like `provider = Request.Content.ReadAsMultipartAsync(provider).Result;`. You can then access the provider after the Wait function. – dan richardson Aug 08 '14 at 11:28
  • 2
    Thanks! This one worked for me `await Task.FromResult(Request.Content.ReadAsMultipartAsync(provider));` – Schnapz Aug 07 '15 at 16:16
  • Gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! Dern you Microsoft. Thanks for posting this, I thought I was losing it. Lost day on this one. – granadaCoder Jan 25 '16 at 18:15
  • The code at the bottom worked for me after I removed the await and async keywords. – Jason May 25 '16 at 18:25
  • +1 is not enough. You are my hero of the day! I was searching for hours, before reading your answer. Thank you so much! – Woozar Mar 28 '18 at 13:57
11

I came across the same problem with all modern 4.5.2 framework.

My API method accepts one or more files uploaded using POST request with multipart content. It worked fine with small files, but with big ones, my method just hanged forever because the ReadAsMultipartAsync() function never completed.

What helped me: using an async controller method and await for the ReadAsMultipartAsync() to complete, instead of getting the task result in a synchronous controller method.

So, this did not work:

[HttpPost]
public IHttpActionResult PostFiles()
{
    return Ok
    (
        Request.Content.ReadAsMultipartAsync().Result

        .Contents
        .Select(content => ProcessSingleContent(content))
    );
}

private string ProcessSingleContent(HttpContent content)
{
    return SomeLogic(content.ReadAsByteArrayAsync().Result);
}

And this worked:

[HttpPost]
public async Task<IHttpActionResult> PostFiles()
{
    return Ok
    (
        await Task.WhenAll
        (
            (await Request.Content.ReadAsMultipartAsync())

            .Contents
            .Select(async content => await ProcessSingleContentAsync(content))  
        )
    );
}

private async Task<string> ProcessSingleContentAsync(HttpContent content)
{
    return SomeLogic(await content.ReadAsByteArrayAsync());
}

where SomeLogic is just a synchronous function taking binary content and producing a string (can be any kind of processing).

UPDATE And finally I've found the explanation in this article: https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

The root cause of this deadlock is due to the way await handles contexts. By default, when an incomplete Task is awaited, the current “context” is captured and used to resume the method when the Task completes. This “context” is the current SynchronizationContext unless it’s null, in which case it’s the current TaskScheduler. GUI and ASP.NET applications have a SynchronizationContext that permits only one chunk of code to run at a time. When the await completes, it attempts to execute the remainder of the async method within the captured context. But that context already has a thread in it, which is (synchronously) waiting for the async method to complete. They’re each waiting for the other, causing a deadlock.

So, basically, the “Async all the way” guideline has a reason behind it, and this is a good example.

C-F
  • 1,597
  • 16
  • 25
4

With help of another answer on stackoverflow and a blog post about targetFramework, I've found that updating to 4.5 and adding/updating the following in your web.config fixes this issue:

<system.web>
    <compilation debug="true" targetFramework="4.5"/>
</system.web>
<appSettings>
    <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
</appSettings>
Community
  • 1
  • 1
Zenuka
  • 3,110
  • 28
  • 39
0

I have a working .Net MVC WebAPi project with the following Post method which seems to work well. It's very similar to what you have already so this should be helpful.

    [System.Web.Http.AcceptVerbs("Post")]
    [System.Web.Http.HttpPost]
    public Task<HttpResponseMessage> Post()
    {
        // Check if the request contains multipart/form-data.
        if (!Request.Content.IsMimeMultipartContent())
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }
        string fileSaveLocation = @"c:\SaveYourFile\Here\XXX";
        CustomMultipartFormDataStreamProvider provider = new CustomMultipartFormDataStreamProvider(fileSaveLocation);
        Task<HttpResponseMessage> task = Request.Content.ReadAsMultipartAsync(provider).ContinueWith<HttpResponseMessage>(t =>
            {
                if (t.IsFaulted || t.IsCanceled)
                {
                    Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception);
                }
                foreach (MultipartFileData file in provider.FileData)
                {
                    //Do Work Here
                }
                return Request.CreateResponse(HttpStatusCode.OK);
            }
        );
        return task;
    }
Camille Sévigny
  • 5,104
  • 4
  • 38
  • 61
0

I had the same. My solution

public List<string> UploadFiles(HttpFileCollection fileCollection)
    {
        var uploadsDirectoryPath = HttpContext.Current.Server.MapPath("~/Uploads");
        if (!Directory.Exists(uploadsDirectoryPath))
            Directory.CreateDirectory(uploadsDirectoryPath);

        var filePaths = new List<string>();

        for (var index = 0; index < fileCollection.Count; index++)
        {
            var path = Path.Combine(uploadsDirectoryPath, Guid.NewGuid().ToString());
            fileCollection[index].SaveAs(path);
            filePaths.Add(path);
        }

        return filePaths;
    }

and invoking

if (!Request.Content.IsMimeMultipartContent())
{
    throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}

var filePaths = _formsService.UploadFiles(HttpContext.Current.Request.Files);