apify/crawlee · error · Error

The "value" parameter must be a String, Buffer, ArrayBuffer,

Error message

The "value" parameter must be a String, Buffer, ArrayBuffer, TypedArray, or Stream when "options.contentType" is specified.

What it means

KeyValueStore.setValue() only accepts a non-string content type option when the value is a string, Buffer, stream, or similar binary-serializable value. If you pass options.contentType with a plain object/number/boolean, the library cannot know how to apply that content type, so it throws instead of silently mis-encoding the value.

Source

Thrown at packages/core/src/storages/key_value_store.ts:523

     * otherwise the crawler process might finish before the value is stored!
     *
     * @param key
     *   Unique key of the record. It can be at most 256 characters long and only consist
     *   of the following characters: `a`-`z`, `A`-`Z`, `0`-`9` and `!-_.'()`
     * @param value
     *   Record data, which can be one of the following values:
     *    - If `null`, the record in the key-value store is deleted.
     *    - If no `options.contentType` is specified, `value` can be any JavaScript object and it will be stringified to JSON.
     *    - If `options.contentType` is set, `value` is taken as is and it must be a `String` or [`Buffer`](https://nodejs.org/api/buffer.html).
     *   For any other value an error will be thrown.
     * @param [options] Record options.
     */
    async setValue<T>(key: string, value: T | null, options: RecordOptions = {}): Promise<void> {
        const transaction = activeStorageTransaction();

        parseArgument(key, setValueKeySchema);
        if (options.contentType && !(typeof value === 'string' || isBuffer(value) || isStream(value))) {
            throw new Error(
                'The "value" parameter must be a String, Buffer, ArrayBuffer, TypedArray, or Stream when "options.contentType" is specified.',
            );
        }
        // The parse result is a fresh copy, so we never update what user passed.
        const optionsCopy = parseArgument(options, recordOptionsSchema);

        // The whole transaction branch sits *above* the auto-saved cache update below, so a buffered
        // write touches nothing outside the journal. That cache is shared, process-lifetime frontend
        // state, so mutating it here would survive a rollback and later be persisted by `persistState`.
        // The commit replay re-enters this method with no active transaction and updates it then.
        if (transaction) {
            if (isStream(value)) {
                // A stream cannot serve both a read-your-own-writes read and the commit replay. The
                // transaction is known-active here, so throw directly rather than via the conditional guard.
                throw operationRejectedInTransaction(
                    `KeyValueStore.setValue() with a stream value (key "${key}")`,
                    'a stream can only be consumed once, so it cannot be buffered until commit.',
                );

View on GitHub (pinned to dbe57fb09c)

Solutions

  1. Pass a string, Buffer, ArrayBuffer, TypedArray, or Stream as value when specifying options.contentType
  2. If the value is an object, drop options.contentType and let it be JSON-serialized automatically
  3. If you need a custom content type for an object, pre-serialize it yourself: setValue(key, JSON.stringify(obj), { contentType: 'application/json' })

Example fix

// before
await store.setValue('logo', { url: 'x' }, { contentType: 'image/png' });
// after
await store.setValue('logo', await fs.readFile('logo.png'), { contentType: 'image/png' });
// or for objects, no contentType:
await store.setValue('config', { url: 'x' });
Defensive patterns

Strategy: type-guard

Validate before calling

function assertStorableWithValueType(value, options = {}) {
  const binary = value == null || typeof value === 'string' || Buffer.isBuffer(value) || value instanceof ArrayBuffer || ArrayBuffer.isView(value) || (value && typeof value.pipe === 'function');
  if (options.contentType && !binary) throw new TypeError('contentType requires a string/Buffer/TypedArray/Stream value');
}

Type guard

const canHaveContentType = (v) => typeof v === 'string' || Buffer.isBuffer(v) || v instanceof ArrayBuffer || ArrayBuffer.isView(v) || (v && typeof v.pipe === 'function');

Try / catch

try {
  await store.setValue(key, value, { contentType });
} catch (e) {
  if (e.message.includes('must be a String, Buffer')) {
    await store.setValue(key, JSON.stringify(value), { contentType: 'application/json' });
  } else throw e;
}

Prevention

When it happens

Trigger: Calling store.setValue('key', { some: 'object' }, { contentType: 'image/png' }) or any non-string value combined with options.contentType. Only string, Buffer, ArrayBuffer/TypedArray, or Stream values are valid with contentType.

Common situations: Trying to store parsed JSON as an image/binary content type; copying a setValue call written for Buffer values and swapping the value for an object; typos where contentType was meant for a different key.

Related errors


AI-assisted analysis of apify/crawlee@dbe57fb09c (2026-08-30). Data as JSON: /api/errors/b4290de6efc9f9cf. Report an issue: GitHub.