mastra-ai/mastra · error

HttpRemoteStrategy: requestContext is not JSON-serializable.

Error message

HttpRemoteStrategy: requestContext is not JSON-serializable. ${err instanceof Error ? err.message : String(err)}

What it means

Before POSTing step params, #buildBody round-trips requestContext through JSON.parse(JSON.stringify(...)) to guarantee a plain serializable payload. If requestContext contains non-JSON values (functions, class instances with circular refs, BigInt, undefined in hostile spots), serialization throws and this descriptive error replaces the opaque JSON error.

Source

Thrown at packages/core/src/worker/strategies/http-remote-strategy.ts:99

    return res.json() as Promise<StepResult<unknown, unknown, unknown, unknown>>;
  }

  /**
   * Build a JSON-serializable request body. The `params.requestContext` is
   * a plain object; if a caller stuffed a non-serializable value into it we
   * surface a clear error instead of silently dropping fields.
   *
   * `abortSignal` is consumed via fetch's `signal` argument — it must not
   * be in the body.
   */
  #buildBody(params: StepExecutionParams): string {
    const { abortSignal: _abortSignal, requestContext, ...rest } = params;
    let safeRequestContext: Record<string, unknown>;
    try {
      safeRequestContext = JSON.parse(JSON.stringify(requestContext ?? {}));
    } catch (err) {
      throw new Error(
        `HttpRemoteStrategy: requestContext is not JSON-serializable. ${err instanceof Error ? err.message : String(err)}`,
      );
    }

    return JSON.stringify({
      ...rest,
      requestContext: safeRequestContext,
    });
  }

  #combineSignals(externalSignal?: AbortSignal): AbortSignal {
    const timeoutSignal = AbortSignal.timeout(this.#timeoutMs);
    if (!externalSignal) return timeoutSignal;
    // AbortSignal.any aborts when any input aborts.
    if (typeof AbortSignal.any === 'function') {
      return AbortSignal.any([timeoutSignal, externalSignal]);
    }
    // Fallback for runtimes without AbortSignal.any

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Ensure requestContext holds only JSON-safe values (strings, numbers, plain objects, arrays)
  2. Remove non-serializable members (clients, loggers, functions) before invoking the step
  3. Convert special types explicitly (BigInt -> string, Date -> ISO string, Map -> Object.fromEntries)

Example fix

// before
requestContext.set('db', prisma);
// after
requestContext.set('dbConnectionString', process.env.DATABASE_URL);
Defensive patterns

Strategy: type-guard

Validate before calling

function isJsonSafe(v: unknown, seen = new Set()): boolean {
  if (v === null || typeof v !== 'object') return typeof v !== 'function' && typeof v !== 'bigint';
  if (seen.has(v)) return false;
  seen.add(v);
  return Object.values(v).every((x) => isJsonSafe(x, seen));
}

Try / catch

try {
  await strategy.executeStep(params);
} catch (e) {
  if (e instanceof Error && e.message.includes('not JSON-serializable')) {
    console.error('requestContext contains non-JSON values:', e.message);
  }
  throw e;
}

Prevention

When it happens

Trigger: Storing a function, class instance, Map/Set, circular object graph, or BigInt in requestContext before executing a step via HttpRemoteStrategy.

Common situations: Passing a logger, DB client, or Prisma client via requestContext; putting the raw Hono/Express request object into context; using context values that worked locally with InProcessStrategy but break over HTTP.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/8f681fe6ea2c804d. Report an issue: GitHub.