Full-resolution proxy route (streaming)
Source: artifex/backend/routes/images.js —
/images/:id/originalCategory: Snippet — HTTP
Full-res proxy route — the endpoint that serves the raw uploaded file when the user clicks “Download” or opens the image in a new tab. Non-trivially correct: auth, streaming, correct headers, graceful 404s, no buffering large files into memory.
Why it’s non-trivial
Section titled “Why it’s non-trivial”Wrong-way 1: res.sendFile(path) from Express — fine for small files; fine for small user counts; breaks on tens of MB images because sendFile buffers reads. Sort of.
Wrong-way 2: fs.readFile(path, (err, data) => res.send(data)) — buffers the entire file in memory. A single 50MB upload triples memory usage; concurrent downloads compound.
Right way: stream. fs.createReadStream(path).pipe(res). Set headers first; the stream pipes bytes out as the disk yields them.
import { stat, createReadStream } from 'fs';import { promisify } from 'util';import mime from 'mime';
const statAsync = promisify(stat);
router.get('/images/:id/original', requireAuth, async (req, res) => { const image = await db.getImage(req.params.id); if (!image) return res.status(404).json({ error: 'Not found' });
// Authorization if (image.visibility !== 'public' && image.user_id !== req.user.id) { return res.status(403).json({ error: 'Forbidden' }); }
let stats; try { stats = await statAsync(image.file_path); } catch { return res.status(404).json({ error: 'File missing' }); }
const contentType = mime.getType(image.file_path) ?? 'application/octet-stream'; res.setHeader('Content-Type', contentType); res.setHeader('Content-Length', stats.size); res.setHeader('Cache-Control', 'private, max-age=3600'); res.setHeader( 'Content-Disposition', req.query.download ? `attachment; filename="${image.original_name}"` : 'inline' );
const stream = createReadStream(image.file_path); stream.on('error', err => { if (!res.headersSent) res.status(500).end(); else res.destroy(err); }); stream.pipe(res);});Range request support (for video)
Section titled “Range request support (for video)”Video files need range requests so browsers can seek. A few more headers:
const range = req.headers.range;if (range) { const [start, end] = parseRange(range, stats.size); res.status(206); res.setHeader('Content-Range', `bytes ${start}-${end}/${stats.size}`); res.setHeader('Content-Length', end - start + 1); res.setHeader('Accept-Ranges', 'bytes'); createReadStream(image.file_path, { start, end }).pipe(res); return;}Gotchas
Section titled “Gotchas”- Set
Content-Lengthor forego it. Omitting it is fine (Express chunks the response), but a known length enables progress bars on the client.stat().sizegives it to you for free. Cache-Control: privateon authenticated files.publiccaches in CDNs and reverse proxies — unauthenticated requests may hit stale auth’d content.privatekeeps it user-scoped.immutableis only safe if the URL changes on content change. Originals keyed by id should NOT beimmutable— a user can delete and re-upload the same id in theory. Skip it;max-ageis enough.- Content-Disposition inline vs attachment.
inlinemakes the browser display it (if it can);attachmentforces a download. Expose via query param rather than separate routes. - Stream error handling is subtle. If the stream errors after headers are sent, you can’t send a 500 — you have to destroy the response. If error comes before
pipe, you still have time to send an error status. - Don’t proxy through your app if a CDN can do it. For high-traffic gallery servers, put Cloudflare or nginx in front of the originals dir and let them stream the bytes. Express is fine up to dozens of req/s; CDNs handle thousands.
- Chunked encoding. If you omit Content-Length, Express uses chunked encoding. Most clients handle this; some proxies break it. Always set Content-Length when you have it.
res.destroy()vsres.end(). On error after bytes flowed,end()leaves the connection hanging in a weird state.destroy()tears it down cleanly.
See also
Section titled “See also”- patterns/sharp-image-pipeline — the sibling derivative routes (smaller files, same shape)
- projects/artifex