@httpland/range-request-middleware
Version:
HTTP range request middleware
286 lines (216 loc) • 8.44 kB
Markdown
# range-request-middleware
[](https://deno.land/x/range_request_middleware)
[](https://doc.deno.land/https/deno.land/x/range_request_middleware/mod.ts)
[](https://github.com/httpland/range-request-middleware/releases)
[](https://codecov.io/gh/httpland/range-request-middleware)
[](https://github.com/httpland/range-request-middleware/blob/main/LICENSE)
[](https://github.com/httpland/range-request-middleware/actions/workflows/test.yaml)
[](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` | `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