Handling Range Specific Content Request in WebAPI ASP.Net

In this article we'll talk about downloading the full and partial contents of a file from a server. Then we'll see if you can do it via WebAPI. Before we jump to the main discussion we'll look at some basic things. So let's use the simple case of when you try to download a file from the server, what a header actually contains in a request and response. For example, you want to download a file, say data.zip. Before sending the request you can run fiddler to trace your request and response.

When you send the request the first time you'll see the Request head will contain information something like this:

GET /files/data.zip HTTP/1.1
Accept-Language: en/us,en
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0

Now if you check the response then It'll look something as in the following:

HTTP/1.1 200 OK
Content-Type: application/x-zip-compressed
Last-Modified: Mon, 18 June 2013 07:15:20 GMT
Accept-Ranges: bytes
ETag: "e2827cfd8e57ce1:0"
Server: Microsoft-IIS/8.0
Date: Mon, 17 Jun 2013 09:01:22 GMT
Content-Length: 62887922

So if you want to know that if the server is available to serve the partial content then look at the attribute:

Accept-Ranges: bytes

If this attribute is present with value bytes then that means the client can request the partial content to download. If this attribute is not present in the response or has a value none then the server won't accept the partial content request.

Partial content request headers

Let's have a look at the partial content headers; how they look like when a request is made for them.

GET /files/data.zip HTTP/1.1
Accept-Language: en/us,en
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0
If-Match: "dfTGdsdIofQXrAY3ZddfSd=="
Range: bytes=22234-

Here if you observe, the Range header has the value specified in Bytes with –. It basically follows the format, for example Range: {from}-{To}. If nothing is specified in the To part then it'll read the file to the end and write it in the response. Otherwise if a value is provided then it'll read that portion only. Now let's look at the response of this request.

HTTP/1.1 206 OK
Content-Range: bytes 22234-62887922/62887923
Content-Length: 62887922
Content-Type: application/x-zip-compressed
Last-Modified: Mon, 18 June 2013 09:15:20 GMT
ETag: "dfTGdsdIofQXrAY3ZddfSd=="
Accept-Ranges: bytes
Binary content of data.zip from byte 500,001 onwards...

If you want to learn more about the range specific request in the HTTP protocol then you can have a look at the HTTP Spec.

Now, let's return to the business; why we are discussing all this stuff. I have a ASP.Net MVC WebAPI that is actually serving me the file based on the query parameters. Now in this case the scenario will change. We cannot send the partial request directly to the Controller. If we do need to write the custom code to handle the partial requests. If you're thinking to write the HttpHandlers at this moment then I should tell you that "Controller are new Handlers in WebAPI". Jeff Fritz wrote a nice article on this discussion.
 
So first you can start creating a simple ASP.Net WebAPI project. Let's say add a DownloadController in the contollers folder. We have a method, say DownloadFile in it. So when we send a request to download a file then it'll look something like this. The following sample is just reading the file from disk and returning the complete file.

  1. public class DownloadController : ApiController  
  2. {  
  3.     public HttpResponse DownloadFile(string fileName)  
  4.      {  
  5.          // put your logic for reading the file from disk and return  
  6.          HttpResponseMessage result = null;  
  7.          var fullFilePath = Path.Combine(this.packageDirectoryFilePath, fileName);  
  8.    
  9.          // Get the complete file  
  10.          FileStream sourceStream = File.Open(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);  
  11.          BufferedStream bs = new BufferedStream(sourceStream);  
  12.    
  13.          result = new HttpResponseMessage(HttpStatusCode.OK);  
  14.          result.Content = new StreamContent(bs);  
  15.    
  16.          result.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeType);  
  17.          result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")  
  18.              {  
  19.                  FileName = fileName  
  20.              };  
  21.    
  22.          return result;  
  23.      }  
  24. }
So to send the partial content request you need to send a partial content request something. I've written a custom function to create and send a partial content request or so to say Range specific request.

Client_Program.cs

 

  1. private static void DownloadFileWithRangeSpecificRequests(string sourceUrl, string destinationPath)  
  2. {  
  3.     long existLen = 0;  
  4.     System.IO.FileStream saveFileStream;  
  5.     if (System.IO.File.Exists(destinationPath))  
  6.     {  
  7.          System.IO.FileInfo fINfo = new System.IO.FileInfo(destinationPath);  
  8.          existLen = fINfo.Length;  
  9.     }  
  10.     if (existLen > 0)  
  11.          saveFileStream = new System.IO.FileStream(destinationPath,  
  12.                                           System.IO.FileMode.Append, System.IO.FileAccess.Write,  
  13.                                           System.IO.FileShare.ReadWrite);  
  14.     else  
  15.          saveFileStream = new System.IO.FileStream(destinationPath,  
  16.                                           System.IO.FileMode.Create, System.IO.FileAccess.Write,  
  17.                                           System.IO.FileShare.ReadWrite);  
  18.     
  19.     System.Net.HttpWebRequest httpWebRequest;  
  20.     System.Net.HttpWebResponse httpWebResponse;  
  21.     httpWebRequest = (System.Net.HttpWebRequest) System.Net.HttpWebRequest.Create(sourceUrl);  
  22.     httpWebRequest.AddRange((int) existLen);  
  23.     System.IO.Stream smRespStream;  
  24.     httpWebResponse = (System.Net.HttpWebResponse) httpWebRequest.GetResponse();  
  25.     smRespStream = httpWebResponse.GetResponseStream();  
  26.     var abc = httpWebRequest.Timeout;  
  27.     
  28.     smRespStream.CopyTo(saveFileStream);  
  29.     saveFileStream.Close();  
  30. }
This will actually read the file from file system and if it's not fully downloaded then send the request from the last byte of the file. So if you try to run the application with the default downloader and the client then you'll always get the complete file. Since the DownloadController doesn't know about the request if it's a Range specific. So let's modify the controller and add some checks to receive the RangeSpecific requests and treat them specially.

Update DownloadController.cs
  1. /// <summary>  
  2. /// Gets the file from server.  
  3. /// </summary>  
  4. /// <param name="fileName">Name of the file.</param>  
  5. /// <returns>response content of file</returns>  
  6. public HttpResponseMessage DownloadFile(string fileName)  
  7. {  
  8.     this.LogRequestHttpHeaders(this.logFilePath, Request);  
  9.   
  10.     HttpResponseMessage result = null;  
  11.     var fullFilePath = Path.Combine(this.packageDirectoryFilePath, fileName);  
  12.   
  13.     if (Request.Headers.Range == null || Request.Headers.Range.Ranges.Count == 0 ||  
  14.         Request.Headers.Range.Ranges.FirstOrDefault().From.Value == 0)  
  15.     {  
  16.         // Get the complete file  
  17.         FileStream sourceStream = File.Open(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);  
  18.         BufferedStream bs = new BufferedStream(sourceStream);  
  19.   
  20.         result = new HttpResponseMessage(HttpStatusCode.OK);  
  21.         result.Content = new StreamContent(bs);  
  22.   
  23.         result.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeType);  
  24.         result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")  
  25.             {  
  26.                 FileName = fileName  
  27.             };  
  28.     }  
  29.     else  
  30.     {  
  31.         // Get the partial part  
  32.         var item = Request.Headers.Range.Ranges.FirstOrDefault();  
  33.         if (item != null && item.From.HasValue)  
  34.         {  
  35.             result = this.GetPartialContent(fileName, item.From.Value);  
  36.         }  
  37.     }  
  38.   
  39.     this.LogResponseHttpHeaders(this.logFilePath, result);  
  40.   
  41.     return result;  
  42. }  
  43.   
  44. /// <summary>  
  45. /// Reads the partial content of physical file.  
  46. /// </summary>  
  47. /// <param name="fileName">Name of the file.</param>  
  48. /// <param name="partial">The partial.</param>  
  49. /// <returns>response content of the file</returns>  
  50. private HttpResponseMessage GetPartialContent(string fileName, long partial)  
  51. {  
  52.     var fullFilePath = Path.Combine(this.packageDirectoryFilePath, fileName);  
  53.     FileInfo fileInfo = new FileInfo(fullFilePath);  
  54.     long startByte = partial;  
  55.   
  56.     Action<Stream, HttpContent, TransportContext> pushContentAction = (outputStream, content, context) =>  
  57.         {  
  58.             try  
  59.             {  
  60.                 var buffer = new byte[65536];  
  61.                 using (var file = File.Open(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))  
  62.                 {  
  63.                     var bytesRead = 1;  
  64.                     file.Seek(startByte, SeekOrigin.Begin);  
  65.                     int length = Convert.ToInt32((fileInfo.Length - 1) - startByte) + 1;  
  66.   
  67.                     while (length > 0 && bytesRead > 0)  
  68.                     {  
  69.                         bytesRead = file.Read(buffer, 0, Math.Min(length, buffer.Length));  
  70.                         outputStream.Write(buffer, 0, bytesRead);  
  71.                         length -= bytesRead;  
  72.                     }  
  73.                 }  
  74.             }  
  75.             catch (HttpException ex)  
  76.             {  
  77.                 this.LogException(ex);  
  78.             }  
  79.             finally  
  80.             {  
  81.                 outputStream.Close();  
  82.             }  
  83.         };  
  84.   
  85.     HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.PartialContent);  
  86.     result.Content = new PushStreamContent(pushContentAction, new MediaTypeHeaderValue(MimeType));  
  87.     result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")  
  88.         {  
  89.             FileName = fileName  
  90.         };  
  91.   
  92.     return result;  
  93. }
So now we have added some conditions to check if a request is range specific or it's a full content request. To server the partial content I've written a custom function GetPartialContent() that uses the file name and the startByte as the argument. Also we have used the PushStreamContent to write the content to the response.

Now your WebAPI is ready to serve the content with a Range specific request.

But to tell you the update this case has already been taken into the account of my Microsoft ASP.Net team and they've included the Byte specific request handlers in the NightlyBuilds that are available via Nugets. But I need to write it as updating the production with the latest Pre-release which could be an impact when the RTM is available.

Complete source code:

DownloadController.cs

Client.cs


Similar Articles