# range-request-middleware

[![deno land](http://img.shields.io/badge/available%20on-deno.land/x-lightgrey.svg?logo=deno)](https://deno.land/x/range_request_middleware)
[![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/range_request_middleware/mod.ts)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/httpland/range-request-middleware)](https://github.com/httpland/range-request-middleware/releases)
[![codecov](https://codecov.io/github/httpland/range-request-middleware/branch/main/graph/badge.svg)](https://codecov.io/gh/httpland/range-request-middleware)
[![GitHub](https://img.shields.io/github/license/httpland/range-request-middleware)](https://github.com/httpland/range-request-middleware/blob/main/LICENSE)

[![test](https://github.com/httpland/range-request-middleware/actions/workflows/test.yaml/badge.svg)](https://github.com/httpland/range-request-middleware/actions/workflows/test.yaml)
[![NPM](https://nodei.co/npm/@httpland/range-request-middleware.png?mini=true)](https://nodei.co/npm/@httpland/range-request-middleware/)

HTTP range request middleware.

Handles range request and partial response.

Compliant with
[RFC 9110, 14. Range Requests](https://www.rfc-editor.org/rfc/rfc9110#section-14)

## Usage

Upon receipt of a range request, if the response [satisfies](#satisfiable) the
range requirement, [convert](#convert) it to a partial response.

```ts
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
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](https://www.rfc-editor.org/rfc/rfc9110.html#name-media-type-multipart-bytera).

```ts
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
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](https://www.rfc-editor.org/rfc/rfc9110#section-14.2-6) and the
response will not [convert](#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](#conditions) is met and the following conditions are **not met**
,[unsatisfiable](https://www.rfc-editor.org/rfc/rfc9110#section-14.1.1-12), and
it is not possible to meet partial response.

- If a valid
  [ranges-specifier](https://www.rfc-editor.org/rfc/rfc9110#rule.ranges-specifier)
  contains at least one satisfactory
  [range-spec](https://www.rfc-editor.org/rfc/rfc9110#rule.ranges-specifier), as
  defined in the indicated
  [range-unit](https://www.rfc-editor.org/rfc/rfc9110#range.units)

In this case, the handler response will [convert](#convert) to
[416(Range Not Satisfiable)](https://www.rfc-editor.org/rfc/rfc9110#status.416)
response.

A example of how unsatisfiable can happen:

If receive un unknown range unit.

```ts
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](#conditions) and [unsatisfiable](#unsatisfiable) are met,
[satisfiable](https://www.rfc-editor.org/rfc/rfc9110#satisfiable), and the
response will [convert](#convert) to
[206(Partial Content)](https://www.rfc-editor.org/rfc/rfc9110#section-15.3.7)
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` &#124; `Promise<Response>` | Return response from range request context. |

The middleware supports the following range request protocols by default:

- `bytes`([ByteRanges](#bytesrange))

### 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](https://www.rfc-editor.org/rfc/rfc9110.html#section-14.1.2).

```ts
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](https://github.com/httpland).

Released under the [MIT](./LICENSE) license
