I have implemented a custom subclass of MultipartStreamProvider in order to write uploaded file data to a custom stream. After writing to the stream, the HttpContext is sometimes lost. The code below is a simplified repro that demonstrates the problem. If the custom stream does not actually do any asynchronous work in WriteAsync (i.e. if it stays on the same thread), then everything works as you'd expect. However, as soon as we introduce some actual async work into WriteAsync (simulated here by Task.Delay), then the HttpContext is (usually) lost. Am I doing something incorrectly, or is this a bug in the Web API framework?

public class TestApiController : ApiController
{
    public class CustomMultipartStreamProvider : MultipartStreamProvider
    {
        private readonly List<string> _fileNames = new List<string>();
        private readonly List<Stream> _fileStreams = new List<Stream>();

        public CustomMultipartStreamProvider()
        {
        }

        public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
        {
            string filename;
            if (IsFileContent(headers, out filename))
            {
                var stream = new CustomStream();
                _fileStreams.Add(stream);
                _fileNames.Add(filename);

                return stream;
            }

            return new MemoryStream();
        }

        private static bool IsFileContent(HttpContentHeaders headers, out string filename)
        {
            var contentDisposition = headers.ContentDisposition;
            if (contentDisposition == null)
            {
                filename = null;
                return false;
            }

            filename = UnquoteToken(contentDisposition.FileName);

            return !string.IsNullOrEmpty(filename);
        }

        private static string UnquoteToken(string token)
        {
            if (string.IsNullOrWhiteSpace(token))
                return token;

            if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1)
                return token.Substring(1, token.Length - 2);

            return token;
        }
    }

    class CustomStream : MemoryStream
    {
        public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
        {
            await Task.Delay(100, cancellationToken); // simulate async work (if this line is commented out, everything works correctly)
            await base.WriteAsync(buffer, offset, count, cancellationToken);
        }
    }

    [Route("api/test/multipart")]
    public async Task<string> PutMultiPart()
    {
        // Check if the request contains multipart/mixed content
        if (!Request.Content.IsMimeMultipartContent("mixed"))
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);

        // Read the multipart data
        var multipartStreamProvider = new CustomMultipartStreamProvider();
        await Request.Content.ReadAsMultipartAsync(multipartStreamProvider);

        if (HttpContext.Current != null)
        {
            return "good";
        }
        else
        {
            return "bad";
        }
    }
}

For testing purposes, the request I am sending is essentially this:

Content-Type: multipart/mixed; boundary=boundary42
--boundary42 
Content-Type: application/json

{
  Description: "test file"
}
--boundary42
Content-Type: application/octet-stream 
Content-Disposition: inline; filename=hello.txt

Hello world.
--boundary42--

Related posts

Recent Viewed