Scanning in-memory Buffers with pompelmi and multer memoryStorage
When you configure multer with memoryStorage(), uploaded files never
touch the filesystem — they land in req.file.buffer as a Node.js
Buffer. This is exactly what you want for serverless deployments,
read-only containers, or any pipeline that should avoid disk I/O.
The catch: pompelmi's original scan() API accepts a file path.
If the file doesn't exist on disk, scan() rejects with
File not found. You'd have to write the buffer to a temp file
yourself, scan it, then clean up — error-prone boilerplate that belongs in
the library, not your application code.
pompelmi v1.3.0 ships scanBuffer() to solve this exactly.
The solution: scanBuffer(buffer, [options])
scanBuffer() accepts a Buffer and returns the same
three typed verdict Symbols as scan():
const { scanBuffer, Verdict } = require('pompelmi');
const result = await scanBuffer(req.file.buffer);
if (result === Verdict.Malicious) throw new Error('Malware detected.');
if (result === Verdict.ScanError) console.warn('Scan could not complete.');
// result === Verdict.Clean — safe to process
Two validation errors surface immediately as rejected Promises:
buffer must be a Buffer— when the argument is not aBufferbuffer is empty— whenbuffer.length === 0
Express + multer memoryStorage example
Replace multer.diskStorage() with multer.memoryStorage()
and swap scan(req.file.path) for scanBuffer(req.file.buffer).
No other changes needed.
const express = require('express');
const multer = require('multer');
const { scanBuffer, Verdict } = require('pompelmi');
// memoryStorage — upload lands in req.file.buffer, never written to disk
const upload = multer({ storage: multer.memoryStorage() });
const app = express();
app.post('/upload', upload.single('file'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded.' });
}
let result;
try {
result = await scanBuffer(req.file.buffer);
} catch (err) {
return res.status(500).json({ error: `Scan failed: ${err.message}` });
}
if (result === Verdict.Malicious) {
return res.status(422).json({ error: 'Malicious file rejected.' });
}
if (result === Verdict.ScanError) {
return res.status(422).json({ error: 'Scan incomplete — rejected as precaution.' });
}
// Clean — store or process req.file.buffer however you need
return res.json({ ok: true, file: req.file.originalname });
});
app.listen(3000);
multer.memoryStorage() keeps the entire upload in RAM. Set a
limits.fileSize to prevent memory exhaustion from oversized uploads:
multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } }).
TCP mode vs local mode
scanBuffer() behaves differently depending on whether you provide
a host or port option.
TCP mode — no disk I/O
When host or port is set, the buffer is streamed
directly to a running clamd daemon using the ClamAV
INSTREAM protocol. The buffer is chunked into 64 KB pieces, each
prefixed with a 4-byte big-endian length header, and terminated with four zero
bytes. No data is written to disk at any point.
// Points at a clamd sidecar (e.g. Docker Compose service or Kubernetes pod)
const result = await scanBuffer(req.file.buffer, {
host: '127.0.0.1',
port: 3310,
timeout: 30_000,
});
Local mode — temp file, auto-cleaned
Without host or port, pompelmi writes the buffer to
a randomly-named temp file under os.tmpdir(), calls
clamscan on it, and deletes the file in a finally
block — so the cleanup happens regardless of whether the scan succeeds,
returns an error verdict, or throws.
// Uses local clamscan binary — temp file created and deleted automatically const result = await scanBuffer(req.file.buffer);
If your deployment already runs clamscan locally, local mode
requires no configuration changes. For containers without a local ClamAV
install, TCP mode is the right choice.
Full error handling
const { scanBuffer, Verdict } = require('pompelmi');
async function safeScanBuffer(buffer) {
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
throw new Error('Invalid buffer');
}
try {
const result = await scanBuffer(buffer);
if (result === Verdict.ScanError) {
// ClamAV could not complete the scan — treat as untrusted
console.warn('Scan incomplete — rejecting as precaution.');
return null;
}
return result; // Verdict.Clean or Verdict.Malicious
} catch (err) {
// Buffer validation failed, clamd unreachable, clamscan not in PATH, etc.
console.error('Scan threw:', err.message);
return null;
}
}