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 a Buffer
  • buffer is empty — when buffer.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;
  }
}