{"id":1994,"date":"2012-11-23T08:35:15","date_gmt":"2012-11-23T08:35:15","guid":{"rendered":"https:\/\/blogs.msdn.microsoft.com\/webdev\/2012\/11\/23\/asp-net-web-api-and-http-byte-range-support\/"},"modified":"2012-11-23T08:35:15","modified_gmt":"2012-11-23T08:35:15","slug":"asp-net-web-api-and-http-byte-range-support","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/asp-net-web-api-and-http-byte-range-support\/","title":{"rendered":"ASP.NET Web API and HTTP Byte Range Support"},"content":{"rendered":"<p>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 \u201cbytes\u201d. An example of a range request asking for the first 10 bytes is as follows:<\/p>\n<p><font face=\"Courier New\">&#160; GET \/api\/range HTTP\/1.1      <br \/>&#160; Host: localhost:50231       <br \/>&#160; Range : bytes=0-9<\/font><\/p>\n<p>An example asking for the first and last byte contains two ranges separated by comma as follows:<\/p>\n<p> <font face=\"Courier New\">&#160; GET \/api\/range HTTP\/1.1    <br \/>&#160; Host: localhost:50231     <br \/>&#160; Range : bytes=0-0, -1<\/font>   <\/p>\n<p>In this example the resource which we are doing range requests over contains the 26 letters of the English alphabet:<\/p>\n<p><font face=\"Courier New\">&#160; HTTP\/1.1 200 OK      <br \/>&#160; Content-Length: 26       <br \/>&#160; Content-Type: text\/plain       <\/p>\n<p>&#160; abcdefghijklmnopqrstuvwxyz<\/font><\/p>\n<p>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:<\/p>\n<p><font face=\"Courier New\">&#160; HTTP\/1.1 206 Partial Content      <br \/>&#160; Content-Length: 10       <br \/>&#160; Content-Type: text\/plain       <br \/>&#160; <\/font><font face=\"Courier New\">Content-Range: bytes 0-9\/26      <\/p>\n<p>&#160; <\/font><font face=\"Courier New\">abcdefghij<\/font><\/p>\n<p>Note that the Content-Length header indicates the bytes actually in the response and not the total size of the document requested. <\/p>\n<p>If more than one ranges were requested then the response has the media type \u201cmultipart\/byteranges\u201d with a body part for each range:<\/p>\n<p><font face=\"Courier New\">&#160; HTTP\/1.1 206 Partial Content      <br \/>&#160; Content-Length: 244       <br \/>&#160; Content-Type: multipart\/byteranges; boundary=&quot;57c2656a-9716-4ea0-9d3b-2f76cbac4885&quot;<\/font><\/p>\n<p><font face=\"Courier New\">&#160; &#8211;57c2656a-9716-4ea0-9d3b-2f76cbac4885      <br \/>&#160; Content-Type: text\/plain       <br \/>&#160; Content-Range: bytes 0-0\/26       <\/p>\n<p>&#160; a       <br \/>&#160; &#8211;57c2656a-9716-4ea0-9d3b-2f76cbac4885       <br \/>&#160; Content-Type: text\/plain       <br \/>&#160; Content-Range: bytes 25-25\/26       <\/p>\n<p>&#160; z       <br \/>&#160; &#8211;57c2656a-9716-4ea0-9d3b-2f76cbac4885&#8211;<\/font>     <\/p>\n<p>Range requests that don\u2019t 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.<\/p>\n<p><font face=\"Courier New\">&#160; HTTP\/1.1 416 Requested Range Not Satisfiable      <br \/>&#160; Content-Range: bytes *\/26<\/font><\/p>\n<p>In addition to using ranges as described above, range requests can be made conditional using an If-Range header field meaning \u201csend me the following range but only if the ETag matches; otherwise send me the whole response.\u201d<\/p>\n<p>With the addition of the <a href=\"http:\/\/aspnetwebstack.codeplex.com\/SourceControl\/BrowseLatest#src\/System.Net.Http.Formatting\/ByteRangeStreamContent.cs\">ByteRangeStreamContent<\/a> class to ASP.NET Web API (<a href=\"http:\/\/blogs.msdn.com\/b\/henrikn\/archive\/2012\/06\/01\/using-nightly-asp-net-web-stack-nuget-packages-with-vs-2012-rc.aspx\" target=\"_blank\" rel=\"noopener\">available in latest nightly build, not RTM<\/a>), 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\u2019t show this scenario in this blog.<\/p>\n<p>The ByteRangeStreamContent is very similar to the already existing <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/system.net.http.streamcontent.aspx\">StreamContent<\/a> 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 <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/system.io.filestream.aspx\">FileStreams<\/a> and <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/system.io.memorystream.aspx\">MemoryStreams<\/a>. In this blog we show an example using a MemoryStream but a FileStream or other seekable stream would work just as well.<\/p>\n<h2>The Range Controller<\/h2>\n<p>Below is the sample controller. It is part of the <a href=\"http:\/\/blogs.msdn.com\/b\/webdev\/archive\/2012\/08\/26\/asp-net-web-api-and-httpclient-samples.aspx\">ASP.NET Web API samples<\/a> where the entire sample project is <a href=\"http:\/\/aspnet.codeplex.com\/SourceControl\/changeset\/view\/17baf56823e7#Samples%2fNet4%2fCS%2fWebApi%2fHttpRangeRequestSample%2fReadMe.txt\">available in our git repository<\/a>.<\/p>\n<div id=\"codeSnippetWrapper\">\n<div id=\"codeSnippet\">\n<pre><span id=\"lnum1\">   1:<\/span> public class RangeController : ApiController<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum2\">   2:<\/span> {<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum3\">   3:<\/span>     \/\/ Sample content used to demonstrate range requests<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum4\">   4:<\/span>     private static readonly byte[] _content = Encoding.UTF8.GetBytes(&quot;abcdefghijklmnopqrstuvwxyz&quot;);<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum5\">   5:<\/span>&#160; <\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum6\">   6:<\/span>     \/\/ Content type for our body<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum7\">   7:<\/span>     private static readonly MediaTypeHeaderValue _mediaType = MediaTypeHeaderValue.Parse(&quot;text\/plain&quot;);<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum8\">   8:<\/span>&#160; <\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum9\">   9:<\/span>     public HttpResponseMessage Get()<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum10\">  10:<\/span>     {<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum11\">  11:<\/span>         \/\/ A MemoryStream is seekable allowing us to do ranges over it. Same goes for FileStream.<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum12\">  12:<\/span>         MemoryStream memStream = new MemoryStream(_content);<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum13\">  13:<\/span>&#160; <\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum14\">  14:<\/span>         \/\/ Check to see if this is a range request (i.e. contains a Range header field)<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum15\">  15:<\/span>         \/\/ Range requests can also be made conditional using the If-Range header field. This can be <\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum16\">  16:<\/span>         \/\/ used to generate a request which says: send me the range if the content hasn't changed; <\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum17\">  17:<\/span>         \/\/ otherwise send me the whole thing.<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum18\">  18:<\/span>         if (Request.Headers.Range != null)<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum19\">  19:<\/span>         {<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum20\">  20:<\/span>             try<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum21\">  21:<\/span>             {<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum22\">  22:<\/span>                 HttpResponseMessage partialResponse = Request.CreateResponse(HttpStatusCode.PartialContent);<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum23\">  23:<\/span>                 partialResponse.Content = new ByteRangeStreamContent(memStream, Request.Headers.Range, _mediaType);<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum24\">  24:<\/span>                 return partialResponse;<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum25\">  25:<\/span>             }<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum26\">  26:<\/span>             catch (InvalidByteRangeException invalidByteRangeException)<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum27\">  27:<\/span>             {<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum28\">  28:<\/span>                 return Request.CreateErrorResponse(invalidByteRangeException);<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum29\">  29:<\/span>             }<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum30\">  30:<\/span>         }<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum31\">  31:<\/span>         else<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum32\">  32:<\/span>         {<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum33\">  33:<\/span>             \/\/ If it is not a range request we just send the whole thing as normal<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum34\">  34:<\/span>             HttpResponseMessage fullResponse = Request.CreateResponse(HttpStatusCode.OK);<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum35\">  35:<\/span>             fullResponse.Content = new StreamContent(memStream);<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum36\">  36:<\/span>             fullResponse.Content.Headers.ContentType = _mediaType;<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum37\">  37:<\/span>             return fullResponse;<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum38\">  38:<\/span>         }<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum39\">  39:<\/span>     }<\/pre>\n<p><!--CRLF--><\/p>\n<pre><span id=\"lnum40\">  40:<\/span> }<\/pre>\n<p><!--CRLF--><\/div>\n<\/div>\n<p>&#160;<\/p>\n<p>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.<\/p>\n<h2>Trying It Out<\/h2>\n<p>Running the sample creates a set of range requests. We write the corresponding responses to the console as follows:<\/p>\n<p><font face=\"Courier New\">&#160; Full Content without ranges: &#8216;abcdefghijklmnopqrstuvwxyz&#8217; <\/p>\n<p>&#160; Range &#8216;bytes=0-0&#8217; requesting the first byte: &#8216;a&#8217; <\/p>\n<p>&#160; Range &#8216;bytes=-1&#8217; requesting the last byte: &#8216;z&#8217; <\/p>\n<p>&#160; Range &#8216;bytes=4-&#8216; requesting byte 4 and up: &#8216;efghijklmnopqrstuvwxyz&#8217; <\/p>\n<p>&#160; Range &#8216;bytes=0-0, -1&#8217; requesting first and last byte: <\/p>\n<p>&#160; &#8211;04214a40-a998-4b9e-a564-c21955bd36db <\/p>\n<p>&#160; Content-Type: text\/plain <\/p>\n<p>&#160; Content-Range: bytes 0-0\/26 <\/p>\n<p>&#160; a <\/p>\n<p>&#160; &#8211;04214a40-a998-4b9e-a564-c21955bd36db <\/p>\n<p>&#160; Content-Type: text\/plain <\/p>\n<p>&#160; Content-Range: bytes 25-25\/26 <\/p>\n<p>&#160; z <\/p>\n<p>&#160; &#8211;04214a40-a998-4b9e-a564-c21955bd36db&#8211; <\/p>\n<p>&#160; Range &#8216;bytes=0-0, 12-15, -1&#8217; requesting first, mid four, and last byte: <\/p>\n<p>&#160; &#8211;b1d1d766-c424-49cb-9843-dd741be35f4c <\/p>\n<p>&#160; Content-Type: text\/plain <\/p>\n<p>&#160; Content-Range: bytes 0-0\/26 <\/p>\n<p>&#160; a <\/p>\n<p>&#160; &#8211;b1d1d766-c424-49cb-9843-dd741be35f4c <\/p>\n<p>&#160; Content-Type: text\/plain <\/p>\n<p>&#160; Content-Range: bytes 12-15\/26 <\/p>\n<p>&#160; mnop <\/p>\n<p>&#160; &#8211;b1d1d766-c424-49cb-9843-dd741be35f4c <\/p>\n<p>&#160; Content-Type: text\/plain <\/p>\n<p>&#160; Content-Range: bytes 25-25\/26 <\/p>\n<p>&#160; z <\/p>\n<p>&#160; &#8211;b1d1d766-c424-49cb-9843-dd741be35f4c&#8211; <\/p>\n<p>&#160; Range &#8216;bytes=100-&#8216; resulted in status code &#8216;RequestedRangeNotSatisfiable&#8217; with <\/p>\n<p>&#160; Content-Range header &#8216;bytes *\/26&#8217;<\/font><\/p>\n<p>Have fun!<\/p>\n<p>Henrik<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":403,"featured_media":58792,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[197],"tags":[31,34,7420,80],"class_list":["post-1994","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aspnet","tag-asp-net","tag-asp-net-web-api","tag-codepl","tag-httpclient"],"acf":[],"blog_post_summary":"<p>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 [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/1994","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/403"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=1994"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/1994\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/58792"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=1994"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=1994"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=1994"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}