Better streaming static file server with Range and conditional-GET support
npx @tessl/cli install tessl/npm-send@1.2.0Send is a Node.js library for streaming files from the file system as HTTP responses with comprehensive support for partial content delivery through HTTP Range requests, conditional-GET negotiation, and granular event handling. It provides configurable options for cache control, ETag generation, dotfile handling, and more.
npm install sendconst send = require('send');For ES modules:
import send from 'send';const http = require('http');
const send = require('send');
const server = http.createServer(function(req, res) {
// Basic file serving
send(req, req.url, { root: '/public' })
.pipe(res);
});
server.listen(3000);Creates a SendStream instance for streaming files to HTTP responses.
/**
* Create a new SendStream for the given path to send to a response
* @param {object} req - HTTP request object
* @param {string} path - URL-encoded path to serve
* @param {object} [options] - Configuration options
* @returns {SendStream} SendStream instance
*/
function send(req, path, options);Usage Examples:
// Basic usage
send(req, '/index.html').pipe(res);
// With options
send(req, pathname, {
root: '/www/public',
index: ['index.html'],
dotfiles: 'deny',
maxAge: '1d'
}).pipe(res);
// With event handling
send(req, pathname, options)
.on('error', function(err) {
res.statusCode = err.status || 500;
res.end(err.message);
})
.on('directory', function(res, path) {
res.statusCode = 301;
res.setHeader('Location', req.url + '/');
res.end('Redirecting to ' + req.url + '/');
})
.pipe(res);The main class returned by the send() factory function, providing file streaming capabilities.
/**
* SendStream constructor (internal - use send() factory function)
* @param {object} req - HTTP request object
* @param {string} path - File path to serve
* @param {object} [options] - Configuration options
*/
class SendStream extends Stream;Pipes the file content to an HTTP response with full request processing including Range requests, conditional-GET, error handling, and header management.
/**
* Pipe to response object
* @param {Stream} res - HTTP response object
* @returns {Stream} The response object
*/
SendStream.prototype.pipe(res);Emits error or sends HTTP error response with appropriate status code and HTML document.
/**
* Emit error with status code
* @param {number} status - HTTP status code
* @param {Error} [err] - Optional error object
*/
SendStream.prototype.error(status, err);Checks if the request includes conditional-GET headers.
/**
* Check if this is a conditional GET request
* @returns {boolean} True if conditional headers present
*/
SendStream.prototype.isConditionalGET();Validates If-Match and If-Unmodified-Since request headers against response ETag and Last-Modified.
/**
* Check if the request preconditions failed
* @returns {boolean} True if preconditions failed
*/
SendStream.prototype.isPreconditionFailure();Checks if the cached response is fresh using conditional headers.
/**
* Check if the cache is fresh
* @returns {boolean} True if cache is fresh
*/
SendStream.prototype.isFresh();Validates If-Range header against ETag or Last-Modified for Range requests.
/**
* Check if the range is fresh
* @returns {boolean} True if range is fresh
*/
SendStream.prototype.isRangeFresh();Sends a 304 Not Modified response and removes content headers.
/**
* Respond with 304 not modified
*/
SendStream.prototype.notModified();Performs directory redirect or emits directory event.
/**
* Redirect to path
* @param {string} path - Path to redirect to
*/
SendStream.prototype.redirect(path);Checks if the pathname ends with a forward slash.
/**
* Check if the pathname ends with "/"
* @returns {boolean} True if ends with slash
*/
SendStream.prototype.hasTrailingSlash();Determines if the response status code is cacheable (2xx or 304).
/**
* Check if the request is cacheable
* @returns {boolean} True if cacheable status code
*/
SendStream.prototype.isCachable();interface SendOptions {
/** Enable or disable accepting ranged requests (default: true) */
acceptRanges?: boolean;
/** Enable or disable setting Cache-Control response header (default: true) */
cacheControl?: boolean;
/** How to treat dotfiles: 'allow', 'deny', or 'ignore' (default: 'ignore') */
dotfiles?: string;
/** Byte offset at which the stream ends (for Range request processing) */
end?: number;
/** Enable or disable etag generation (default: true) */
etag?: boolean;
/** File extensions to try if file doesn't exist */
extensions?: string[];
/** Enable immutable directive in Cache-Control (default: false) */
immutable?: boolean;
/** Index file names or disable with false (default: ['index.html']) */
index?: string[] | string | boolean;
/** Enable or disable Last-Modified header (default: true) */
lastModified?: boolean;
/** Max-age in milliseconds for http caching (default: 0). Alternative: maxage */
maxAge?: number | string;
/** Root directory for relative paths */
root?: string;
/** Byte offset at which the stream starts (default: 0, for Range request processing) */
start?: number;
}Configuration Examples:
// Disable dotfiles and enable caching
const options = {
root: '/public',
dotfiles: 'deny',
maxAge: '1 day',
immutable: true
};
// Custom index files and extensions
const options = {
root: '/public',
index: ['index.html', 'default.html'],
extensions: ['html', 'htm']
};
// Range request configuration
const options = {
acceptRanges: true,
start: 100, // Start at byte 100
end: 1000 // End at byte 1000
};The SendStream instance emits the following events:
interface SendStreamEvents {
/** Error occurred during processing */
'error': (err: Error) => void;
/** Directory was requested */
'directory': (res: Response, path: string) => void;
/** File was requested and found */
'file': (path: string, stat: fs.Stats) => void;
/** Headers are about to be set on a file */
'headers': (res: Response, path: string, stat: fs.Stats) => void;
/** File streaming has started */
'stream': (stream: fs.ReadStream) => void;
/** Streaming has completed */
'end': () => void;
}Event Handling Examples:
send(req, pathname, options)
.on('error', function(err) {
// Handle errors (404, 403, 500, etc.)
res.statusCode = err.status || 500;
res.end(err.message);
})
.on('directory', function(res, path) {
// Handle directory requests
res.statusCode = 301;
res.setHeader('Location', req.url + '/');
res.end('Redirecting to ' + req.url + '/');
})
.on('file', function(path, stat) {
// Log file access
console.log('Serving file:', path, 'Size:', stat.size);
})
.on('headers', function(res, path, stat) {
// Customize headers before they're sent
if (path.endsWith('.pdf')) {
res.setHeader('Content-Disposition', 'attachment');
}
})
.on('stream', function(stream) {
// Handle streaming start
console.log('Started streaming file');
})
.on('end', function() {
// Handle streaming completion
console.log('Finished streaming file');
})
.pipe(res);Send provides comprehensive error handling with appropriate HTTP status codes:
interface SendErrors {
/** 400 Bad Request - Malformed URI, null bytes */
BadRequest: 400;
/** 403 Forbidden - Malicious path, denied dotfiles, directory without trailing slash */
Forbidden: 403;
/** 404 Not Found - File not found, ignored dotfiles */
NotFound: 404;
/** 412 Precondition Failed - Conditional request preconditions failed */
PreconditionFailed: 412;
/** 416 Range Not Satisfiable - Invalid range request */
RangeNotSatisfiable: 416;
/** 500 Internal Server Error - File system errors, headers already sent */
InternalServerError: 500;
}
interface SendError extends Error {
/** HTTP status code */
status: number;
/** Error code */
code?: string;
/** File system path that caused the error */
path?: string;
}Error Handling Patterns:
// Automatic error handling (default)
send(req, pathname, options).pipe(res);
// Custom error handling
send(req, pathname, options)
.on('error', function(err) {
if (err.status === 404) {
// Serve custom 404 page
res.statusCode = 404;
res.end('Custom not found page');
} else {
// Handle other errors
res.statusCode = err.status || 500;
res.end(err.message);
}
})
.pipe(res);const path = require('path');
send(req, pathname, { root: '/public' })
.on('headers', function(res, filePath) {
const ext = path.extname(filePath);
switch (ext) {
case '.x-custom':
res.setHeader('Content-Type', 'application/x-custom-type');
break;
}
})
.pipe(res);const fs = require('fs');
send(req, pathname, { index: false, root: '/public' })
.once('directory', function(res, dirPath) {
const stream = this;
// Redirect to trailing slash for consistent URLs
if (!stream.hasTrailingSlash()) {
return stream.redirect(dirPath);
}
// Custom directory listing
fs.readdir(dirPath, function(err, list) {
if (err) return stream.error(500, err);
res.setHeader('Content-Type', 'text/plain; charset=UTF-8');
res.end(list.join('\\n') + '\\n');
});
})
.pipe(res);// Enable range requests for video streaming
send(req, pathname, {
root: '/media',
acceptRanges: true
})
.on('headers', function(res, path, stat) {
// Add custom headers for media files
if (path.match(/\\.(mp4|webm)$/)) {
res.setHeader('Accept-Ranges', 'bytes');
}
})
.pipe(res);// Aggressive caching for static assets
send(req, pathname, {
root: '/static',
maxAge: '1 year',
immutable: true,
etag: true,
lastModified: true
})
.pipe(res);