ASP.NET Web API and HTTP Byte Range Support

Henrik F Nielsen

Range requests is the ability in HTTP to request a part of a document based on one or more ranges. This can be used in scenarios where the client wants to recover from interrupted data transfers as a result of canceled requests or dropped connections. It can also be used in scenarios where a client requests only a subset of a larger representation, such as a single segment of a very large document for special processing. Ranges specify a start and an end given a unit. The unit can be anything but by far the most common is “bytes”. An example of a range request asking for the first 10 bytes is as follows:

  GET /api/range HTTP/1.1
  Host: localhost:50231
  Range : bytes=0-9

An example asking for the first and last byte contains two ranges separated by comma as follows:

  GET /api/range HTTP/1.1
  Host: localhost:50231
  Range : bytes=0-0, -1

In this example the resource which we are doing range requests over contains the 26 letters of the English alphabet:

  HTTP/1.1 200 OK
  Content-Length: 26
  Content-Type: text/plain

  abcdefghijklmnopqrstuvwxyz

The response to a byte range request is a 206 (Partial Content) response. If only one range was requested then the response looks similar to a 200 (OK) response with the exception that it has a Content-Range header field indicating the range and the total length of the document:

  HTTP/1.1 206 Partial Content
  Content-Length: 10
  Content-Type: text/plain
 
Content-Range: bytes 0-9/26

  abcdefghij

Note that the Content-Length header indicates the bytes actually in the response and not the total size of the document requested.

If more than one ranges were requested then the response has the media type “multipart/byteranges” with a body part for each range:

  HTTP/1.1 206 Partial Content
  Content-Length: 244
  Content-Type: multipart/byteranges; boundary="57c2656a-9716-4ea0-9d3b-2f76cbac4885"

  –57c2656a-9716-4ea0-9d3b-2f76cbac4885
  Content-Type: text/plain
  Content-Range: bytes 0-0/26

  a
  –57c2656a-9716-4ea0-9d3b-2f76cbac4885
  Content-Type: text/plain
  Content-Range: bytes 25-25/26

  z
  –57c2656a-9716-4ea0-9d3b-2f76cbac4885–

Range requests that don’t overlap with the extent of the resource result in a 416 (Requested Range Not Satisfiable) with a Content-Range header indicating the current extent of the resource.

  HTTP/1.1 416 Requested Range Not Satisfiable
  Content-Range: bytes */26

In addition to using ranges as described above, range requests can be made conditional using an If-Range header field meaning “send me the following range but only if the ETag matches; otherwise send me the whole response.”

With the addition of the ByteRangeStreamContent class to ASP.NET Web API (available in latest nightly build, not RTM), it is now simpler to support byte range requests. The ByteRangeStreamContent class can also be used in scenarios supporting conditional If-Range requests although we don’t show this scenario in this blog.

The ByteRangeStreamContent is very similar to the already existing StreamContent in that it provides a view over a stream. ByteRangeStreamContent requires the stream to be seekable in order to provide one or more ranges over it. Common examples of seekable streams are FileStreams and MemoryStreams. In this blog we show an example using a MemoryStream but a FileStream or other seekable stream would work just as well.

The Range Controller

Below is the sample controller. It is part of the ASP.NET Web API samples where the entire sample project is available in our git repository.

   1: public class RangeController : ApiController

   2: {

   3:     // Sample content used to demonstrate range requests

   4:     private static readonly byte[] _content = Encoding.UTF8.GetBytes("abcdefghijklmnopqrstuvwxyz");

   5:  

   6:     // Content type for our body

   7:     private static readonly MediaTypeHeaderValue _mediaType = MediaTypeHeaderValue.Parse("text/plain");

   8:  

   9:     public HttpResponseMessage Get()

  10:     {

  11:         // A MemoryStream is seekable allowing us to do ranges over it. Same goes for FileStream.

  12:         MemoryStream memStream = new MemoryStream(_content);

  13:  

  14:         // Check to see if this is a range request (i.e. contains a Range header field)

  15:         // Range requests can also be made conditional using the If-Range header field. This can be 

  16:         // used to generate a request which says: send me the range if the content hasn't changed; 

  17:         // otherwise send me the whole thing.

  18:         if (Request.Headers.Range != null)

  19:         {

  20:             try

  21:             {

  22:                 HttpResponseMessage partialResponse = Request.CreateResponse(HttpStatusCode.PartialContent);

  23:                 partialResponse.Content = new ByteRangeStreamContent(memStream, Request.Headers.Range, _mediaType);

  24:                 return partialResponse;

  25:             }

  26:             catch (InvalidByteRangeException invalidByteRangeException)

  27:             {

  28:                 return Request.CreateErrorResponse(invalidByteRangeException);

  29:             }

  30:         }

  31:         else

  32:         {

  33:             // If it is not a range request we just send the whole thing as normal

  34:             HttpResponseMessage fullResponse = Request.CreateResponse(HttpStatusCode.OK);

  35:             fullResponse.Content = new StreamContent(memStream);

  36:             fullResponse.Content.Headers.ContentType = _mediaType;

  37:             return fullResponse;

  38:         }

  39:     }

  40: }

 

The first thing to check is if the incoming request is a range request. If it is then we create a ByteRangeStreamContent and return that. Otherwise we create a StreamContent and return that. The ByteRangeStreamContent throws an InvalidByteRangeException is no overlapping ranges are found so we catch that and create a 416 (Requested Range Not Satisfiable) response.

Trying It Out

Running the sample creates a set of range requests. We write the corresponding responses to the console as follows:

  Full Content without ranges: ‘abcdefghijklmnopqrstuvwxyz’

  Range ‘bytes=0-0’ requesting the first byte: ‘a’

  Range ‘bytes=-1’ requesting the last byte: ‘z’

  Range ‘bytes=4-‘ requesting byte 4 and up: ‘efghijklmnopqrstuvwxyz’

  Range ‘bytes=0-0, -1’ requesting first and last byte:

  –04214a40-a998-4b9e-a564-c21955bd36db

  Content-Type: text/plain

  Content-Range: bytes 0-0/26

  a

  –04214a40-a998-4b9e-a564-c21955bd36db

  Content-Type: text/plain

  Content-Range: bytes 25-25/26

  z

  –04214a40-a998-4b9e-a564-c21955bd36db–

  Range ‘bytes=0-0, 12-15, -1’ requesting first, mid four, and last byte:

  –b1d1d766-c424-49cb-9843-dd741be35f4c

  Content-Type: text/plain

  Content-Range: bytes 0-0/26

  a

  –b1d1d766-c424-49cb-9843-dd741be35f4c

  Content-Type: text/plain

  Content-Range: bytes 12-15/26

  mnop

  –b1d1d766-c424-49cb-9843-dd741be35f4c

  Content-Type: text/plain

  Content-Range: bytes 25-25/26

  z

  –b1d1d766-c424-49cb-9843-dd741be35f4c–

  Range ‘bytes=100-‘ resulted in status code ‘RequestedRangeNotSatisfiable’ with

  Content-Range header ‘bytes */26’

Have fun!

Henrik

0 comments

Discussion is closed.

Feedback usabilla icon