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.1Host: 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, 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 which we write 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