/range-request-middleware

HTTP range request middleware

Primary LanguageTypeScriptMIT LicenseMIT

range-request-middleware

deno land deno doc GitHub release (latest by date) codecov GitHub

test NPM

HTTP range request middleware.

Handles range request and partial response.

Compliant with RFC 9110, 14. Range Requests

Usage

Upon receipt of a range request, if the response satisfies the range requirement, convert it to a partial response.

import { rangeRequest } from "https://deno.land/x/range_request_middleware@$VERSION/middleware.ts";
import {
  assert,
  assertEquals,
  assertThrows,
} from "https://deno.land/std/testing/asserts.ts";

const middleware = rangeRequest();
const request = new Request("test:", {
  headers: { range: "bytes=5-9" },
});
const response = await middleware(
  request,
  () => new Response("abcdefghijklmnopqrstuvwxyz"),
);

assertEquals(response.status, 206);
assertEquals(response.headers.get("content-range"), "bytes 5-9/26");
assertEquals(response.headers.get("accept-ranges"), "bytes");
assertEquals(await response.text(), "fghij");

yield:

HTTP/1.1 206
Content-Range: bytes 5-9/26
Accept-Ranges: bytes

fghij

Multi-range request

For multi-range request, response body will convert to a multipart content.

It compliant with RFC 9110, 14.6. Media Type multipart/byteranges.

import { rangeRequest } from "https://deno.land/x/range_request_middleware@$VERSION/middleware.ts";
import {
  assert,
  assertEquals,
  assertThrows,
} from "https://deno.land/std/testing/asserts.ts";

const middleware = rangeRequest();
const request = new Request("test:", {
  headers: { range: "bytes=5-9, 20-, -5" },
});
const response = await middleware(
  request,
  () => new Response("abcdefghijklmnopqrstuvwxyz"),
);

assertEquals(response.status, 206);
assertEquals(
  response.headers.get(
    "content-type",
  ),
  "multipart/byteranges; boundary=<boundary-delimiter>",
);
assertEquals(
  await response.text(),
  `--<boundary-delimiter>
Content-Type: text/plain;charset=UTF-8
Content-Range: 5-9/26

fghij
--<boundary-delimiter>
Content-Type: text/plain;charset=UTF-8
Content-Range: 20-25/26

uvwxyz
--<boundary-delimiter>
Content-Type: text/plain;charset=UTF-8
Content-Range: 21-25/26

vwxyz
--<boundary-delimiter>--`,
);

yield:

HTTP/1.1 206
Content-Type: multipart/byteranges; boundary=BOUNDARY
Accept-Ranges: bytes

--BOUNDARY
Content-Type: text/plain;charset=UTF-8
Content-Range: 5-9/26

fghij
--BOUNDARY
Content-Type: text/plain;charset=UTF-8
Content-Range: 20-25/26

uvwxyz
--BOUNDARY
Content-Type: text/plain;charset=UTF-8
Content-Range: 21-25/26

vwxyz
--BOUNDARY--

Conditions

There are several conditions that must be met in order for middleware to execute.

If the following conditions are not met, invalid and the response will not convert.

  • Request method is GET.
  • Request includes Range header
  • Request does not include If-Range header
  • Request Range header is valid syntax
  • Request Range header is valid semantics
  • Response status code is 200
  • Response does not include Content-Range header
  • Response does not include Accept-Ranges header or its value is not none
  • Response body is readable

Note that if there is an If-Range header, do nothing.

Unsatisfiable

If conditions is met and the following conditions are not met ,unsatisfiable, and it is not possible to meet partial response.

In this case, the handler response will convert to 416(Range Not Satisfiable) response.

A example of how unsatisfiable can happen:

If receive un unknown range unit.

import {
  type Handler,
  rangeRequest,
} from "https://deno.land/x/range_request_middleware@$VERSION/mod.ts";
import { assert, assertEquals } from "https://deno.land/std/testing/asserts.ts";

declare const handler: Handler;
const middleware = rangeRequest();
const response = await middleware(
  new Request("test:", { headers: { range: "<unknown-unit>=<other-range>" } }),
  handler,
);

assertEquals(response.status, 416);
assert(response.headers.has("content-range"));

Satisfiable

If the conditions and unsatisfiable are met, satisfiable, and the response will convert to 206(Partial Content) response.

Convert

Convert means a change without side effects.

For example, "convert a response to the 206 response" means to return a new response in which some or all of the following elements have been replaced from the original response.

  • HTTP Content
  • HTTP Status code
  • HTTP Headers(shallow marge)

Range

Range abstracts partial response.

Middleware factories can accept Range objects and implement own range request protocols.

Range is the following structure:

Name Type Description
rangeUnit string Corresponding range unit.
respond (response: Response, context: RangesSpecifier) => Response | Promise<Response> Return response from range request context.

The middleware supports the following range request protocols by default:

BytesRange

bytes range unit is used to express subranges of a representation data's octet sequence.

ByteRange supports single and multiple range requests.

Compliant with RFC 9110, 14.1.2. Byte Ranges.

import {
  BytesRange,
  type IntRange,
  type SuffixRange,
} from "https://deno.land/x/range_request_middleware@$VERSION/mod.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

const bytesRange = new BytesRange();
const rangeUnit = "bytes";
declare const initResponse: Response;
declare const rangeSet: [IntRange, SuffixRange];

const response = await bytesRange.respond(initResponse, {
  rangeUnit,
  rangeSet,
});

assertEquals(bytesRange.rangeUnit, rangeUnit);
assertEquals(response.status, 206);
assertEquals(
  response.headers.get("content-type"),
  "multipart/byteranges; boundary=<BOUNDARY>",
);

Effects

Middleware may make changes to the following HTTP messages:

  • HTTP Content
  • HTTP Headers
    • Accept-Ranges
    • Content-Range
    • Content-Type
  • HTTP Status code
    • 206(Partial Content)
    • 416(Range Not Satisfiable)

License

Copyright © 2023-present httpland.

Released under the MIT license