{"id":"a8b5272fcc3c35d9","repo":"redis/node-redis","slug":"himport-prepare-discard-discardall-are-not-support","errorCode":null,"errorMessage":"HIMPORT PREPARE/DISCARD/DISCARDALL are not supported inside MULTI/pipeline; call them on the client before the transaction","messagePattern":"HIMPORT PREPARE/DISCARD/DISCARDALL are not supported inside MULTI/pipeline; call them on the client before the transaction","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/client/lib/client/index.ts","lineNumber":52,"sourceCode":"import { ASKING_CMD } from '../commands/ASKING';\n\nconst noop = () => {};\n\nconst HIMPORT_SESSION_SUBCOMMANDS = new Set(['PREPARE', 'DISCARD', 'DISCARDALL']);\n\n/**\n * MULTI/pipeline stores raw args only, so the HIMPORT transparency hook never sees these\n * commands — a PREPARE/DISCARD executed that way would silently diverge the client registry\n * from server state (a fieldset the registry doesn't know about, or a discarded one it\n * would resurrect via lazy prepare). Rejected client-side at the exec funnel, which covers\n * both the typed multi methods and raw `multi.addCommand(...)`. HIMPORT SET stays allowed:\n * it mutates no registry state (the fieldset must already exist on the carrying connection).\n */\nfunction assertNoHimportSessionCommands(commands: Array<RedisMultiQueuedCommand>) {\n  for (const { args } of commands) {\n    if (String(args[0]).toUpperCase() !== 'HIMPORT') continue;\n    if (HIMPORT_SESSION_SUBCOMMANDS.has(String(args[1]).toUpperCase())) {\n      throw new Error(\n        'HIMPORT PREPARE/DISCARD/DISCARDALL are not supported inside MULTI/pipeline; call them on the client before the transaction'\n      );\n    }\n  }\n}\n\nexport interface RedisClientOptions<\n  M extends RedisModules = RedisModules,\n  F extends RedisFunctions = RedisFunctions,\n  S extends RedisScripts = RedisScripts,\n  RESP extends RespVersions = 3,\n  TYPE_MAPPING extends TypeMapping = TypeMapping,\n  SocketOptions extends RedisSocketOptions = RedisSocketOptions\n> extends CommanderConfig<M, F, S, RESP> {\n  /**\n   * `redis[s]://[[username][:password]@][host][:port][/db-number]`\n   * See [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details\n   */","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/redis/node-redis/blob/bb5beb56578573910e2ee8f39681edc214c41398/packages/client/lib/client/index.ts#L34-L70","documentation":"assertNoHimportSessionCommands runs at the exec/pipeline funnel (_executeMulti / _executePipeline) and rejects any queued HIMPORT PREPARE, DISCARD, or DISCARDALL. MULTI/pipeline only stores raw args, so the HIMPORT transparency hook never runs for these — executing them in a transaction would silently desync the client-side fieldset registry from the server's session state (a fieldset the registry doesn't know about, or a discarded one it would resurrect via lazy prepare). HIMPORT SET is allowed because it mutates no registry state.","triggerScenarios":"`client.multi().hImportPrepare(...).exec()`; `client.multi().addCommand(['HIMPORT','PREPARE',...]).exec()`; building a pipeline that includes HIMPORT DISCARD/DISCARDALL; calling the typed hImportDiscard/hImportDiscardAll/hImportPrepare methods through a multi/transaction chain.","commonSituations":"Refactoring HIMPORT workflow to run inside a transaction for atomicity; copy-pasting a sequence of HIMPORT calls into a multi block; a generic pipeline builder that indiscriminately queues every queued command.","solutions":["Call HIMPORT PREPARE / DISCARD / DISCARDALL on the client directly (not inside multi/pipeline) before starting the transaction.","Move only HIMPORT SET (and other non-session HIMPORT subcommands) into the MULTI/pipeline block.","If you queue commands generically, filter HIMPORT session subcommands out of pipeline/multi batches."],"exampleFix":"// before\nawait client.multi()\n  .hImportPrepare('myset')\n  .hImportSet('myset', data)\n  .exec();\n\n// after\nawait client.hImportPrepare('myset');\nawait client.multi()\n  .hImportSet('myset', data)\n  .exec();","handlingStrategy":"validation","validationCode":"const HIMPORT_SESSION = new Set(['PREPARE', 'DISCARD', 'DISCARDALL']);\nfunction isHimportSessionCommand(args) {\n  return String(args[0]).toUpperCase() === 'HIMPORT' && HIMPORT_SESSION.has(String(args[1]).toUpperCase());\n}\n// filter your pipeline/multi batch:\nconst safe = commands.filter(c => !isHimportSessionCommand(c.args));","typeGuard":"function isHimportSessionSubcommand(args: ReadonlyArray<unknown>): boolean {\n  return String(args[0]).toUpperCase() === 'HIMPORT' &&\n    new Set(['PREPARE', 'DISCARD', 'DISCARDALL']).has(String(args[1]).toUpperCase());\n}","tryCatchPattern":"try {\n  await multi.exec();\n} catch (err) {\n  if (err instanceof Error && /HIMPORT PREPARE\\/DISCARD/.test(err.message)) {\n    // move HIMPORT session commands out of the transaction and retry\n  } else throw err;\n}","preventionTips":["Call HIMPORT PREPARE/DISCARD/DISCARDALL on the client, not inside multi/pipeline.","Only queue HIMPORT SET inside transactions.","Filter HIMPORT session subcommands out of generic pipeline builders."],"tags":["himport","multi","pipeline","validation"],"analyzedSha":"bb5beb56578573910e2ee8f39681edc214c41398","analyzedAt":"2026-08-03T19:09:15.686Z","schemaVersion":2}