{"record":{"id":"daf2307cab448be3","repo":"apify/crawlee","slug":"operation-cannot-be-used-inside-a-storage-trans","errorCode":null,"errorMessage":"${operation} cannot be used inside a storage transaction: ${reason} If you really need it, wrap the call in withDirectStorageAccess(() => ...) - operations performed there are applied immediately and are not rolled back.","messagePattern":"(.+?) cannot be used inside a storage transaction: (.+?) If you really need it, wrap the call in withDirectStorageAccess\\(\\(\\) => \\.\\.\\.\\) - operations performed there are applied immediately and are not rolled back\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/storages/key_value_store.ts","lineNumber":538,"sourceCode":"\n        parseArgument(key, setValueKeySchema);\n        if (options.contentType && !(typeof value === 'string' || isBuffer(value) || isStream(value))) {\n            throw new Error(\n                'The \"value\" parameter must be a String, Buffer, ArrayBuffer, TypedArray, or Stream when \"options.contentType\" is specified.',\n            );\n        }\n        // The parse result is a fresh copy, so we never update what user passed.\n        const optionsCopy = parseArgument(options, recordOptionsSchema);\n\n        // The whole transaction branch sits *above* the auto-saved cache update below, so a buffered\n        // write touches nothing outside the journal. That cache is shared, process-lifetime frontend\n        // state, so mutating it here would survive a rollback and later be persisted by `persistState`.\n        // The commit replay re-enters this method with no active transaction and updates it then.\n        if (transaction) {\n            if (isStream(value)) {\n                // A stream cannot serve both a read-your-own-writes read and the commit replay. The\n                // transaction is known-active here, so throw directly rather than via the conditional guard.\n                throw operationRejectedInTransaction(\n                    `KeyValueStore.setValue() with a stream value (key \"${key}\")`,\n                    'a stream can only be consumed once, so it cannot be buffered until commit.',\n                );\n            }\n\n            // Validation only, result discarded: the journal snapshot (`structuredClone`) accepts values\n            // JSON cannot, which would otherwise only throw at a later read or at commit.\n            if (value !== null) {\n                serializeValue(value, optionsCopy.contentType);\n            }\n\n            // One snapshot serves both the reads and the commit replay; `null` is a tombstone.\n            transaction.recordJournalEntry({\n                type: 'keyValueStore',\n                participant: this,\n                storageId: this.id,\n                key,\n                value: value === null ? null : snapshotValue(value),","sourceCodeStart":520,"sourceCodeEnd":556,"githubUrl":"https://github.com/apify/crawlee/blob/dbe57fb09ca607ad59dcf998f3925ef9ac3bb26c/packages/core/src/storages/key_value_store.ts#L520-L556","documentation":"This error is thrown by `KeyValueStore.setValue()` when it is called inside an active storage transaction (`transaction !== undefined`) with a value that cannot participate in the transaction. For stream values the store throws unconditionally via `operationRejectedInTransaction` because a stream can only be consumed once and cannot be buffered until commit (it could not serve both read-your-own-writes and the commit replay). The error tells you to wrap the call in `withDirectStorageAccess()` so the write is applied immediately and is not subject to rollback.","triggerScenarios":"Calling `keyValueStore.setValue(key, someStream)` (or any setValue whose value is detected as a stream via `isStream(value)`) inside a `useStorageTransaction()`/transactional block. Callers like `ensurePersistStateEvent` and `commitJournalEntries` route through setValue, so any transactional code path that passes a stream hits this.","commonSituations":"Storing a `ReadableStream`, Node `Readable`, or file stream (e.g. a downloaded file or generated binary blob) into the key-value store while inside a run-scoped storage transaction introduced for atomic state persistence; code that worked before transactions were added starts failing after being wrapped in a transaction.","solutions":["Wrap the `setValue(...)` call in `withDirectStorageAccess(() => ...)` so the stream write is applied immediately outside transaction buffering.","Convert the stream to a Buffer/string (e.g. via `streamToBuffer` or consuming it fully) before `setValue`, since non-stream values can be journaled and replayed at commit.","Move the stream write out of the transactional section entirely, performing it before opening or after committing the transaction."],"exampleFix":"// before\nawait useStorageTransaction(async (tx) => {\n    await tx.setValue('report.pdf', pdfStream); // throws\n});\n\n// after\nawait withDirectStorageAccess(async () => {\n    await store.setValue('report.pdf', pdfStream);\n});","handlingStrategy":"validation","validationCode":"// Before calling setValue inside a transaction, reject stream values\nimport { isStream } from '@crawlee/core/utils';\nif (isStream(value)) {\n  throw new TypeError(`Value for key \"${key}\" is a stream; buffer it or use withDirectStorageAccess first.`);\n}\nawait store.setValue(key, value);","typeGuard":"function isBufferableValue(v: unknown): v is string | Buffer | Record<string, unknown> {\n  return typeof v === 'string' || Buffer.isBuffer(v) || (typeof v === 'object' && v !== null && !('pipe' in v) && !(v instanceof ReadableStream));\n}","tryCatchPattern":"try {\n  await store.setValue(key, value);\n} catch (err) {\n  if (String(err.message).includes('cannot be used inside a storage transaction')) {\n    // Stream value inside a transaction: apply directly, outside the journal\n    await withDirectStorageAccess(() => store.setValue(key, value));\n  } else {\n    throw err;\n  }\n}","preventionTips":["Never pass streams to `setValue`; consume them into Buffer/string first so values can be journaled and replayed at commit.","Keep side-effectful, non-bufferable writes in `withDirectStorageAccess` blocks from the start of the design.","Audit code that was wrapped in `useStorageTransaction` for `setValue` calls with binary/stream payloads.","Write a unit test that exercises transactional setValue paths with all value types you support."],"tags":["storage","transaction","key-value-store","stream"],"backgroundTag":"operation-not-allowed-in-storage-transaction","analyzedSha":"dbe57fb09ca607ad59dcf998f3925ef9ac3bb26c","analyzedAt":"2026-08-30T22:22:28.328Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}