Express.js/connect middleware for serving partial content (206) byte-range responses from buffers or streams
Need to support seeking in a (reproducible) buffer or stream? Attach this middleware to your GET
route and you can now res.sendSeekable
your resource:
const Express = require('express')
const sendSeekable = require('send-seekable');
const app = new Express();
app.use(sendSeekable);
const exampleBuffer = new Buffer('Weave a circle round him thrice');
// this route accepts HTTP request with Range header, e.g. `bytes=10-15`
app.get('/', function (req, res, next) {
res.sendSeekable(exampleBuffer);
})
app.listen(1337);
npm install send-seekable --save
- Node version 0.12.0 or higher
GET
andHEAD
requests (the latter is handled automatically by Express; the server will still produce the necessary buffer or stream as if preparing for aGET
, but will refrain from actually transmitting the body)- Sending buffers (as they are)
- Sending streams (requires predetermined metadata content length, in bytes)
- Byte range requests
- From a given byte:
bytes=2391-
- From a given byte to a later byte:
bytes=3340-7839
- The last X bytes:
bytes=-4936
- From a given byte:
- Does not handle multi-range requests (
bytes=834-983,1056-1181,1367-
) - Does not cache buffers or streams; you must provide a buffer or stream containing identical content upon each request to a specific route
HTTP clients sometimes request a portion of a resource, to cut down on transmission time, payload size, and/or server processing. A typical example is an HTML5 audio
element with a src
set to a route on your server. Clicking on the audio progress bar ideally allows the browser to seek to that section of the audio file. To do so, the browser may send an HTTP request with a Range
header specifying which bytes are desired.
Express.js automatically handles range requests for routes terminating in a res.sendFile
. This is relatively easy to support as the underlying fs.createReadStream
can be called with start
and end
bytes. However, Express does not natively support range requests for buffers or streams. This makes sense: for buffers, you need to either re-create/fetch the buffer (custom logic) or cache it (bad for memory). For streams it is even harder: streams don't know their total byte size, they can't "rewind" to an earlier portion, and they cannot be cached as simply as buffers.
Regardless, sometimes you can't — or won't — store a resource on disk. Provided you can re-create the stream or buffer, it would be convenient for Express to slice the content to the client's desired range. This module enables that.
const sendSeekable = require('send-seekable');
A Connect/Express-style middleware function. It simply adds the method res.sendSeekable
, which you can call as needed.
Attaching sendSeekable
as app-wide middleware is an easy way to "set and forget." Your app and routes work exactly as they did before; you must deliberately call res.sendSeekable
to actually change a route's behavior.
// works for all routes in this app / sub-routers
app.use(sendSeekable);
// works for all routes in this router / sub-routers
router.use(sendSeekable);
Alternatively, if you only need to support seeking for a small number of routes, you can attach the middleware selectively — adding the res.sendSeekable
method just where needed. In practice however there is no performance difference.
// attached to this specific route
app.get('/', sendSeekable, function (req, res, next){ /* ... */ });
// also attached to this route
router.get('/', sendSeekable, function (req, res, next) { /* ... */ });
Param | Type | Details |
---|---|---|
`stream | buffer` | A Node.js Stream instance or Buffer instance |
config |
Object |
Optional for buffers; required for streams. Has two properties: .type is the optional MIME-type of the content (e.g. audio/mp4 ), and .length is the total size of the content in bytes (required for streams). More on this below. |
const exampleBuffer = new Buffer('And close your eyes with holy dread');
app.get('/', sendSeekable, function (req, res, next) {
res.sendSeekable(exampleBuffer);
})
With the middleware module mounted, your res
objects now have a new sendSeekable
method which you can use to support partial content requests on either a buffer or stream.
For either case, it is assumed that the buffer or stream contains identical content on every request. If your route dynamically produces buffers or streams containing different content, with different total byte lengths, the client's range requests may not line up with the new content.
As an example: if you have binary data stored in a database, and can fetch it as a Node.js Buffer instance, you can support partial content ranges using res.sendSeekable
.
app.use(sendSeekable);
const exampleBuffer = new Buffer('For he on honey-dew hath fed');
// minimum use pattern
app.get('/', function (req, res, next) {
res.sendSeekable(exampleBuffer);
})
// the buffer does not have to be cached, so long as you always produce or retrieve the same contents
function makeSameBufferEveryTime () {
return new Buffer('And drunk the milk of Paradise');
}
app.get('/', function (req, res, next) {
const newBuffer = makeSameBufferEveryTime();
res.sendSeekable(newBuffer)
})
The config
object is not required for sending buffers, but it is recommended in order to set the MIME-type of your response — especially in the case of sending audio or video.
// with optional MIME-type configured
app.get('/', function (req, res, next) {
const audiBuffer = fetchAudioBuffer();
res.sendSeekable(audioBuffer, { type: 'audio/mp4' });
})
You can also set this using vanilla Express methods, of course.
// with optional MIME-type configured
app.get('/', function (req, res, next) {
const audioBuffer = fetchAudioBuffer();
res.set('Content-Type', 'audio/mp4');
res.sendSeekable(audioBuffer);
})
Sending streams is almost as easy with some significant caveats.
First, you must know the total byte size of your stream contents ahead of time, and specify it as config.length
.
app.get('/', function (req, res, next) {
const audio = instantiateAudioData();
res.sendSeekable(audio.stream, {
type: audio.type, // e.g. 'audio/mp4'
length: audio.size // e.g. 4287092
});
});
Second, note that you CANNOT simply send the same stream object each time; you must re-create a stream representing identical content. So, this will not work:
const audioStream = radioStream(onlineRadioStationURL);
// DOES NOT WORK IF `audioStream` REPRESENTS CHANGNING CONTENT OVER TIME
app.get('/', function (req, res, next) {
res.sendSeekable(audioStream, {
type: 'audio/mp4',
length: 4287092
});
});
Whereas, something like this is ok:
// Works assuming audio file #123 is always the same
app.get('/', function (req, res, next) {
// a new stream with the same contents, every time there is a request
const audioStream = database.fetchAudioFileById(123);
res.sendSeekable(audioStream, {
type: 'audio/mp4',
length: 4287092
});
});
It can be helpful to understand precisely how sendSeekable
works under thw hood. The short explanation is that res.sendSeekable
determines whether a GET
request is a standard content request or range request, sets the response headers accordingly, and slices the content to send if neccessary. A typical sequence of events might look like this:
- CLIENT: makes plain
GET
request to/api/audio/123
- SERVER: routes request to that route
req
andres
objects pass through thesendSeekable
middlewaresendSeekable
: addsres.sendSeekable
method- ROUTE: fetches audio #123 and associated (pre-recorded) metadata such as file size and MIME-type (you are responsible for this logic)
- ROUTE: calls
res.sendSeekable
with the buffer andconfig
object res.sendSeekable
: places theAccept-Ranges: bytes
header onres
res.sendSeekable
: adds appropriateContent-Length
andContent-Type
headersres.sendSeekable
: streams the entire buffer to the client with200
(ok) status- CLIENT: receives entire file from server
- CLIENT: notes the
Accept-Ranges: bytes
header on the response
Next the user attempts to seek in the audio progress bar to a position corresponding to byte 1048250. Note that steps 2–7 are identical to the initial request steps 2–7:
- CLIENT: makes new
GET
request to/api/audio/123
, withRange
header set tobytes=1048250-
(i.e. from byte 1048250 to the end) - SERVER: routes request to that route
req
andres
objects pass through thesendSeekable
middlewaresendSeekable
: placesres.sendSeekable
method- ROUTE: fetches audio #123 and associated (pre-recorded) metadata such as file size and MIME-type (you are responsible for this logic)
- ROUTE: calls
res.sendSeekable
with the buffer andconfig
object res.sendSeekable
: places theAccept-Ranges: bytes
header onres
res.sendSeekable
: parses the range header on the requestres.sendSeekable
: slices the buffer to the requested rangeres.sendSeekable
: sets theContent-Range
header, as well asContent-Length
andContent-Type
res.sendSeekable
: streams the byte range to the client with206
(partial content) status- CLIENT: receives the requested range
Pull requests are welcome. Send-seekable includes a thorough test suite written for the Mocha framework. You may find it easier to develop for Send-seekable by running the test suite in file watch mode via:
npm run develop
Please add to the test specs (in test/test.js
) for any new features / functionality. Pull requests without tests, or with failing tests, will be gently reminded to include tests.
MIT