payloadcms/payload · error · APIError

Attachment content must be a string or a buffer

Error message

Attachment content must be a string or a buffer

What it means

Thrown by the Resend attachment mapper after it has confirmed the attachment uses the `content` branch (not `path`) but the `content` value is neither a `string` nor a `Buffer`. The mapper checks `typeof === 'string'` first, then `instanceof Buffer`, and falls through to this error for any other type (number, object, array, typed array that is not Buffer, etc.). It is a 400 APIError surfaced from the Payload→Resend transport layer.

Source

Thrown at packages/email-resend/src/index.ts:182

        path,
      }
    }

    if (typeof attachment.content === 'string') {
      return {
        content: attachment.content,
        filename: attachment.filename,
      }
    }

    if (attachment.content instanceof Buffer) {
      return {
        content: attachment.content,
        filename: attachment.filename,
      }
    }

    throw new APIError('Attachment content must be a string or a buffer', 400)
  })
}

function mapHeaders(headers: SendEmailOptions['headers']): Record<string, string> | undefined {
  if (!headers) {
    return undefined
  }

  // Array-of-objects form: [{ key: string; value: string }, ...]
  if (Array.isArray(headers)) {
    return headers.reduce<Record<string, string>>((acc, { key, value }) => {
      acc[key] = value
      return acc
    }, {})
  }

  // Object form: { [key: string]: string | string[] | { prepared: boolean; value: string } }
  return Object.entries(headers).reduce<Record<string, string>>((acc, [key, value]) => {

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Convert the value to a Buffer before attaching: `content: Buffer.from(uint8array)` or `Buffer.from(arrayBuffer)`.
  2. If content is JSON or text, pass it as a string: `content: JSON.stringify(obj)`.
  3. Type the attachment content field strictly as `string | Buffer` in your own call site and enforce it before calling sendEmail.

Example fix

// before
attachments: [{ filename: 'sig.bin', content: crypto.getRandomValues(new Uint8Array(16)) }]

// after
attachments: [{ filename: 'sig.bin', content: Buffer.from(crypto.getRandomValues(new Uint8Array(16))) }]
Defensive patterns

Strategy: type-guard

Validate before calling

function toBufferContent(content) {
  if (typeof content === 'string') return content
  if (content instanceof Buffer) return content
  if (ArrayBuffer.isView(content)) return Buffer.from(content.buffer, content.byteOffset, content.byteLength)
  if (content instanceof ArrayBuffer) return Buffer.from(content)
  throw new Error('attachment content must be string or Buffer')
}

Type guard

function isStringOrBuffer(v: unknown): v is string | Buffer {
  return typeof v === 'string' || Buffer.isBuffer(v)
}

Try / catch

try {
  await payload.sendEmail({ ..., attachments: [{ filename, content: toBufferContent(raw) }] })
} catch (e) {
  if (e instanceof APIError && /content must be a string or a buffer/.test(e.message)) {
    // normalize content before retry
  }
  throw e
}

Prevention

When it happens

Trigger: Setting `attachment.content` to a non-string/non-Buffer value such as a number, plain object, array, or a Uint8Array/ArrayBuffer (which are NOT instances of Buffer). For example `attachments: [{ filename: 'd.bin', content: new Uint8Array([1,2,3]) }]` triggers it because `Uint8Array` is not `Buffer`.

Common situations: Browser-side code or Web Crypto / fetch APIs that return `Uint8Array`/`ArrayBuffer` instead of Node `Buffer`; passing a parsed JSON number/object directly as attachment content; assuming all typed arrays are Buffers; receiving content from a stream and not wrapping it with `Buffer.from()`.

Related errors


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/3ab52bdb228059cc. Report an issue: GitHub.