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
- Pass a string, Buffer, ArrayBuffer, TypedArray, or Stream as value when specifying options.contentType
- If the value is an object, drop options.contentType and let it be JSON-serialized automatically
- 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
- Only set contentType for string/binary values
- Let objects be auto-serialized to JSON without contentType
- Read the value back with getValue to confirm round-trip
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
- Failed to infer format from the path: '${path}'. Supported f
- Unsupported format: '${format}'. Use one of ${supportedForma
- Invalid "proxyUrl". Unsupported protocol: ${proxyUrl}.
- Invalid "proxyUrl" option: authentication is only supported
- The project name cannot be empty string.
AI-assisted analysis of apify/crawlee@dbe57fb09c (2026-08-30).
Data as JSON: /api/errors/b4290de6efc9f9cf.
Report an issue: GitHub.