inngest
Version:
Official SDK for Inngest.com. Inngest is the reliability layer for modern applications. Inngest combines durable execution, events, and queues into a zero-infra platform with built-in observability.
1 lines • 115 kB
Source Map (JSON)
{"version":3,"file":"InngestCommHandler.cjs","names":["stringify","z","allProcessEnv","logPrefix","internalLoggerSymbol","acc","envKeys","defaultStreamingOption: typeof this.streaming","parseAsBoolean","hashEventKey","hashSigningKey","queryKeys","headerKeys","ServerTiming","forwardedHeaders","inngestHeaders","InngestFunction","defaultMaxRetries","AsyncResponseType","v","body","internalEvents","getAsyncCtx","ExecutionVersion","StepMode","result","undefinedToNull","redirectUrl: string","method","headers: Record<string, string>","PREFERRED_ASYNC_EXECUTION_VERSION","signature: string | undefined","serializeError","signature","actionResponseVersion: ExecutionVersion | null | undefined","chainResult: Promise<Middleware.Response>","requestInfo: Middleware.Request","isRecord","buildWrapRequestChain","shouldStream: boolean","createStream","rethrowError","actions: HandlerResponseWithErrors","args","runAsPromise","fn: { fn: InngestFunction.Any; onFailure: boolean } | undefined","fnId: string | undefined","probe","enumFromValue","probeEnum","headerReqVersion: ExecutionVersion | undefined","createVersionSchema","runCompleteOp: OutgoingOp","_internals","StepOpCode","deployId","syncKind","inBandSyncRequestBodySchema","parseFnData","version","fetchAllFnData","data","executionOptions: CreateExecutionOptions","functionConfigSchema","body: InBandRegisterRequest","getPlatformName","introspection:\n | UnauthenticatedIntrospection\n | AuthenticatedIntrospection","res: globalThis.Response","fetchWithAuthFallback","err: unknown","data: z.input<typeof registerResSchema>","status: number","error: string","skipped: boolean","modified: boolean","checkModeConfiguration","signDataWithKey","#verifySignature"],"sources":["../../src/components/InngestCommHandler.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport {\n defaultMaxRetries,\n ExecutionVersion,\n envKeys,\n forwardedHeaders,\n headerKeys,\n internalEvents,\n logPrefix,\n probe as probeEnum,\n queryKeys,\n syncKind,\n} from \"../helpers/consts.ts\";\nimport { enumFromValue } from \"../helpers/enum.ts\";\nimport {\n allProcessEnv,\n checkModeConfiguration,\n type Env,\n getPlatformName,\n inngestHeaders,\n parseAsBoolean,\n} from \"../helpers/env.ts\";\nimport { rethrowError, serializeError } from \"../helpers/errors.ts\";\nimport {\n createVersionSchema,\n type FnData,\n fetchAllFnData,\n parseFnData,\n undefinedToNull,\n} from \"../helpers/functions.ts\";\nimport { warnOnce } from \"../helpers/log.ts\";\nimport { fetchWithAuthFallback, signDataWithKey } from \"../helpers/net.ts\";\nimport { runAsPromise } from \"../helpers/promises.ts\";\nimport { ServerTiming } from \"../helpers/ServerTiming.ts\";\nimport { createStream } from \"../helpers/stream.ts\";\nimport { hashEventKey, hashSigningKey, stringify } from \"../helpers/strings.ts\";\nimport { isRecord, type MaybePromise } from \"../helpers/types.ts\";\nimport type { Logger } from \"../middleware/logger.ts\";\nimport {\n type APIStepPayload,\n AsyncResponseType,\n type AsyncResponseValue,\n type AuthenticatedIntrospection,\n type EventPayload,\n type FunctionConfig,\n functionConfigSchema,\n type InBandRegisterRequest,\n inBandSyncRequestBodySchema,\n type OutgoingOp,\n type RegisterOptions,\n type RegisterRequest,\n StepMode,\n StepOpCode,\n type UnauthenticatedIntrospection,\n} from \"../types.ts\";\nimport { version } from \"../version.ts\";\nimport { getAsyncCtx } from \"./execution/als.ts\";\nimport { _internals } from \"./execution/engine.ts\";\nimport {\n type ExecutionResult,\n type ExecutionResultHandler,\n type ExecutionResultHandlers,\n type InngestExecutionOptions,\n PREFERRED_ASYNC_EXECUTION_VERSION,\n} from \"./execution/InngestExecution.ts\";\nimport { type Inngest, internalLoggerSymbol } from \"./Inngest.ts\";\nimport {\n type CreateExecutionOptions,\n InngestFunction,\n} from \"./InngestFunction.ts\";\nimport { buildWrapRequestChain, type Middleware } from \"./middleware/index.ts\";\n\n// A response object for when an internal server error occurs. When that\n// happens, we don't to leak any internal details to the client.\nconst internalServerErrorResponse = {\n body: stringify({ code: \"internal_server_error\" }),\n headers: { \"Content-Type\": \"application/json\" },\n status: 500,\n version: undefined,\n} as const;\n\n/**\n * A set of options that can be passed to a serve handler, intended to be used\n * by internal and custom serve handlers to provide a consistent interface.\n *\n * @public\n */\nexport interface ServeHandlerOptions extends RegisterOptions {\n /**\n * The `Inngest` instance used to declare all functions.\n */\n client: Inngest.Like;\n\n /**\n * An array of the functions to serve and register with Inngest.\n */\n functions: readonly InngestFunction.Like[];\n}\n\n/**\n * Parameters passed to the asyncRedirectUrl function.\n */\nexport interface AsyncRedirectUrlParams {\n /**\n * The unique identifier for this run.\n */\n runId: string;\n\n /**\n * The token used to authenticate the request to fetch run output.\n */\n token: string;\n}\n\nexport interface SyncHandlerOptions extends RegisterOptions {\n /**\n * The `Inngest` instance used to declare all functions.\n */\n client: Inngest.Like;\n\n /**\n * The type of response you wish to return to an API endpoint when using steps\n * within it and we must transition to {@link StepMode.Async}.\n *\n * In most cases, this defaults to {@link AsyncResponseType.Redirect}.\n */\n asyncResponse?: AsyncResponseValue;\n\n /**\n * Custom URL to redirect to when switching from sync to async mode.\n *\n * Can be:\n * - A string path (e.g., \"/api/inngest/poll\") - resolved relative to request origin\n * - A function that receives `{ runId, token }` and returns a full URL\n *\n * When a string path is provided, `runId` and `token` query parameters are\n * automatically appended.\n *\n * @example\n * ```ts\n * // String path - resolved relative to request origin\n * asyncRedirectUrl: \"/api/inngest/poll\"\n *\n * // Function - full control over URL construction\n * asyncRedirectUrl: ({ runId, token }) =>\n * `https://my-app.com/poll?run=${runId}&t=${token}`\n * ```\n */\n asyncRedirectUrl?:\n | string\n | ((params: AsyncRedirectUrlParams) => string | Promise<string>);\n\n /**\n * If defined, this sets the function ID that represents this endpoint.\n * Without this set, it defaults to using the detected method and path of the\n * request, for example: `GET /api/my-endpoint`.\n */\n functionId?: string;\n\n /**\n * Specifies the maximum number of retries for all steps.\n *\n * Can be a number from `0` to `20`. Defaults to `3`.\n */\n retries?:\n | 0\n | 1\n | 2\n | 3\n | 4\n | 5\n | 6\n | 7\n | 8\n | 9\n | 10\n | 11\n | 12\n | 13\n | 14\n | 15\n | 16\n | 17\n | 18\n | 19\n | 20;\n}\n\nexport type SyncAdapterOptions = Omit<SyncHandlerOptions, \"client\">;\n\nexport interface InternalServeHandlerOptions extends ServeHandlerOptions {\n /**\n * Can be used to override the framework name given to a particular serve\n * handler.\n */\n frameworkName?: string;\n\n /**\n * Can be used to force the handler to always execute functions regardless of\n * the request method or other factors.\n *\n * This is primarily intended for use with Inngest in APIs, where requests may\n * not have the usual shape of an Inngest payload, but we want to pull data\n * and execute.\n */\n // forceExecution?: boolean;\n}\n\ninterface InngestCommHandlerOptions<\n // biome-ignore lint/suspicious/noExplicitAny: intentional\n Input extends any[] = any[],\n // biome-ignore lint/suspicious/noExplicitAny: intentional\n Output = any,\n // biome-ignore lint/suspicious/noExplicitAny: intentional\n StreamOutput = any,\n> extends RegisterOptions {\n /**\n * The name of the framework this handler is designed for. Should be\n * lowercase, alphanumeric characters inclusive of `-` and `/`.\n *\n * This should never be defined by the user; a {@link ServeHandler} should\n * abstract this.\n */\n frameworkName: string;\n\n /**\n * The name of this serve handler, e.g. `\"My App\"`. It's recommended that this\n * value represents the overarching app/service that this set of functions is\n * being served from.\n *\n * This can also be an `Inngest` client, in which case the name given when\n * instantiating the client is used. This is useful if you're sending and\n * receiving events from the same service, as you can reuse a single\n * definition of Inngest.\n */\n client: Inngest.Like;\n\n /**\n * An array of the functions to serve and register with Inngest.\n */\n functions?: readonly InngestFunction.Like[];\n\n /**\n * The `handler` is the function that will be called with your framework's\n * request arguments and returns a set of functions that the SDK will use to\n * access various parts of the request, such as the body, headers, and query\n * string parameters.\n *\n * It also defines how to transform a response from the SDK into a response\n * that your framework can understand, ensuring headers, status codes, and\n * body are all set correctly.\n *\n * @example\n * ```ts\n * function handler (req: Request, res: Response) {\n * return {\n * method: () => req.method,\n * body: () => req.json(),\n * headers: (key) => req.headers.get(key),\n * url: () => req.url,\n * transformResponse: ({ body, headers, status }) => {\n * return new Response(body, { status, headers });\n * },\n * };\n * };\n * ```\n *\n * See any existing handler for a full example.\n */\n handler: Handler<Input, Output, StreamOutput>;\n\n skipSignatureValidation?: boolean;\n\n /**\n * Options for when this comm handler executes a synchronous (API) function.\n */\n syncOptions?: SyncHandlerOptions;\n}\n\n/**\n * A schema for the response from Inngest when registering.\n */\nconst registerResSchema = z.object({\n status: z.number().default(200),\n skipped: z.boolean().optional().default(false),\n modified: z.boolean().optional().default(false),\n error: z.string().default(\"Successfully registered\"),\n});\n\n/**\n * `InngestCommHandler` is a class for handling incoming requests from Inngest (or\n * Inngest's tooling such as the dev server or CLI) and taking appropriate\n * action for any served functions.\n *\n * All handlers (Next.js, RedwoodJS, Remix, Deno Fresh, etc.) are created using\n * this class; the exposed `serve` function will - most commonly - create an\n * instance of `InngestCommHandler` and then return `instance.createHandler()`.\n *\n * See individual parameter details for more information, or see the\n * source code for an existing handler, e.g.\n * {@link https://github.com/inngest/inngest-js/blob/main/src/next.ts}\n *\n * @example\n * ```\n * // my-custom-handler.ts\n * import {\n * InngestCommHandler,\n * type ServeHandlerOptions,\n * } from \"./components/InngestCommHandler\";\n *\n * export const serve = (options: ServeHandlerOptions) => {\n * const handler = new InngestCommHandler({\n * frameworkName: \"my-custom-handler\",\n * ...options,\n * handler: (req: Request) => {\n * return {\n * body: () => req.json(),\n * headers: (key) => req.headers.get(key),\n * method: () => req.method,\n * url: () => new URL(req.url, `https://${req.headers.get(\"host\") || \"\"}`),\n * transformResponse: ({ body, status, headers }) => {\n * return new Response(body, { status, headers });\n * },\n * };\n * },\n * });\n *\n * return handler.createHandler();\n * };\n * ```\n *\n * @public\n */\nexport class InngestCommHandler<\n // biome-ignore lint/suspicious/noExplicitAny: intentional\n Input extends any[] = any[],\n // biome-ignore lint/suspicious/noExplicitAny: intentional\n Output = any,\n // biome-ignore lint/suspicious/noExplicitAny: intentional\n StreamOutput = any,\n> {\n /**\n * The handler specified during instantiation of the class.\n */\n public readonly handler: Handler;\n\n /**\n * The URL of the Inngest function registration endpoint.\n */\n private readonly inngestRegisterUrl: URL;\n\n /**\n * The name of the framework this handler is designed for. Should be\n * lowercase, alphanumeric characters inclusive of `-` and `/`.\n */\n protected readonly frameworkName: string;\n\n /**\n * The origin used to access the Inngest serve endpoint, e.g.:\n *\n * \"https://myapp.com\" or \"https://myapp.com:1234\"\n *\n * By default, the library will try to infer this using request details such\n * as the \"Host\" header and request path, but sometimes this isn't possible\n * (e.g. when running in a more controlled environments such as AWS Lambda or\n * when dealing with proxies/redirects).\n *\n * Provide the custom origin here to ensure that the path is reported\n * correctly when registering functions with Inngest.\n *\n * To also provide a custom path, use `servePath`.\n */\n private readonly _serveOrigin: string | undefined;\n\n /**\n * The path to the Inngest serve endpoint. e.g.:\n *\n * \"/some/long/path/to/inngest/endpoint\"\n *\n * By default, the library will try to infer this using request details such\n * as the \"Host\" header and request path, but sometimes this isn't possible\n * (e.g. when running in a more controlled environments such as AWS Lambda or\n * when dealing with proxies/redirects).\n *\n * Provide the custom path (excluding the hostname) here to ensure that the\n * path is reported correctly when registering functions with Inngest.\n *\n * To also provide a custom hostname, use `serveOrigin`.\n */\n private readonly _servePath: string | undefined;\n\n protected readonly streaming: RegisterOptions[\"streaming\"];\n\n /**\n * A private collection of just Inngest functions, as they have been passed\n * when instantiating the class.\n */\n private readonly rawFns: InngestFunction.Any[];\n\n private readonly client: Inngest.Any;\n\n /**\n * A private collection of functions that are being served. This map is used\n * to find and register functions when interacting with Inngest Cloud.\n */\n private readonly fns: Record<\n string,\n { fn: InngestFunction.Any; onFailure: boolean }\n > = {};\n\n private env: Env = allProcessEnv();\n\n private allowExpiredSignatures: boolean;\n\n private readonly _options: InngestCommHandlerOptions<\n Input,\n Output,\n StreamOutput\n >;\n\n private readonly skipSignatureValidation: boolean;\n\n constructor(options: InngestCommHandlerOptions<Input, Output, StreamOutput>) {\n // Set input options directly so we can reference them later\n this._options = options;\n\n /**\n * v2 -> v3 migration error.\n * TODO: do we need to handle people going from v2->v4?\n *\n * If a serve handler is passed a client as the first argument, it'll be\n * spread in to these options. We should be able to detect this by picking\n * up a unique property on the object.\n */\n if (Object.hasOwn(options, \"eventKey\")) {\n throw new Error(\n `${logPrefix} You've passed an Inngest client as the first argument to your serve handler. This is no longer supported in v3; please pass the Inngest client as the \\`client\\` property of an options object instead. See https://www.inngest.com/docs/sdk/migration`,\n );\n }\n\n this.frameworkName = options.frameworkName;\n this.client = options.client as Inngest.Any;\n\n this.handler = options.handler as Handler;\n\n /**\n * Provide a hidden option to allow expired signatures to be accepted during\n * testing.\n */\n this.allowExpiredSignatures = Boolean(\n // biome-ignore lint/complexity/noArguments: intentional\n arguments[\"0\"]?.__testingAllowExpiredSignatures,\n );\n\n // Ensure we filter any undefined functions in case of missing imports.\n this.rawFns = (options.functions?.filter(Boolean) ??\n []) as InngestFunction.Any[];\n\n if (this.rawFns.length !== (options.functions ?? []).length) {\n this.client[internalLoggerSymbol].warn(\n `Some functions passed to serve() are undefined and misconfigured. Please check your imports.`,\n );\n }\n\n this.fns = this.rawFns.reduce<\n Record<string, { fn: InngestFunction.Any; onFailure: boolean }>\n >((acc, fn) => {\n const configs = fn[\"getConfig\"]({\n baseUrl: new URL(\"https://example.com\"),\n appPrefix: this.client.id,\n });\n\n const fns = configs.reduce((acc, { id }, index) => {\n return { ...acc, [id]: { fn, onFailure: Boolean(index) } };\n }, {});\n\n // biome-ignore lint/complexity/noForEach: intentional\n configs.forEach(({ id }) => {\n if (acc[id]) {\n throw new Error(\n `Duplicate function ID \"${id}\"; please change a function's name or provide an explicit ID to avoid conflicts.`,\n );\n }\n });\n\n return {\n ...acc,\n ...fns,\n };\n }, {});\n\n this.inngestRegisterUrl = new URL(\"/fn/register\", this.client.apiBaseUrl);\n\n this._serveOrigin =\n options.serveOrigin || this.env[envKeys.InngestServeOrigin];\n this._servePath = options.servePath || this.env[envKeys.InngestServePath];\n\n this.skipSignatureValidation = options.skipSignatureValidation || false;\n\n const defaultStreamingOption: typeof this.streaming = false;\n this.streaming = z\n .boolean()\n .default(defaultStreamingOption)\n .catch((ctx) => {\n this.client[internalLoggerSymbol].warn(\n { input: ctx.input, default: defaultStreamingOption },\n \"Unknown streaming option; using default\",\n );\n\n return defaultStreamingOption;\n })\n .parse(\n options.streaming || parseAsBoolean(this.env[envKeys.InngestStreaming]),\n );\n\n // Early validation for environments where process.env is available (Node.js).\n // Edge environments will skip this and validate at request time instead.\n this.client.setEnvVars(this.env);\n }\n\n /**\n * The origin used to access the Inngest serve endpoint, e.g.:\n *\n * \"https://myapp.com\"\n *\n * By default, the library will try to infer this using request details such\n * as the \"Host\" header and request path, but sometimes this isn't possible\n * (e.g. when running in a more controlled environments such as AWS Lambda or\n * when dealing with proxies/redirects).\n *\n * Provide the custom origin here to ensure that the path is reported\n * correctly when registering functions with Inngest.\n *\n * To also provide a custom path, use `servePath`.\n */\n protected get serveOrigin(): string | undefined {\n if (this._serveOrigin) {\n return this._serveOrigin;\n }\n\n const envOrigin = this.env[envKeys.InngestServeOrigin];\n if (envOrigin) {\n return envOrigin;\n }\n\n const envHost = this.env[envKeys.InngestServeHost];\n if (envHost) {\n warnOnce(\n this.client[internalLoggerSymbol],\n \"serve-host-deprecated\",\n \"INNGEST_SERVE_HOST is deprecated; use INNGEST_SERVE_ORIGIN instead\",\n );\n return envHost;\n }\n\n return undefined;\n }\n\n /**\n * The path to the Inngest serve endpoint. e.g.:\n *\n * \"/some/long/path/to/inngest/endpoint\"\n *\n * By default, the library will try to infer this using request details such\n * as the \"Host\" header and request path, but sometimes this isn't possible\n * (e.g. when running in a more controlled environments such as AWS Lambda or\n * when dealing with proxies/redirects).\n *\n * Provide the custom path (excluding the hostname) here to ensure that the\n * path is reported correctly when registering functions with Inngest.\n *\n * To also provide a custom hostname, use `serveOrigin`.\n *\n * This is a getter to encourage checking the environment for the serve path\n * each time it's accessed, as it may change during execution.\n */\n protected get servePath(): string | undefined {\n return this._servePath || this.env[envKeys.InngestServePath];\n }\n\n private get hashedEventKey(): string | undefined {\n if (!this.client.eventKey) {\n return undefined;\n }\n return hashEventKey(this.client.eventKey);\n }\n\n // hashedSigningKey creates a sha256 checksum of the signing key with the\n // same signing key prefix.\n private get hashedSigningKey(): string | undefined {\n if (!this.client.signingKey) {\n return undefined;\n }\n return hashSigningKey(this.client.signingKey);\n }\n\n private get hashedSigningKeyFallback(): string | undefined {\n if (!this.client.signingKeyFallback) {\n return undefined;\n }\n return hashSigningKey(this.client.signingKeyFallback);\n }\n\n /**\n * Returns a `boolean` representing whether this handler will stream responses\n * or not. Takes into account the user's preference and the platform's\n * capabilities.\n */\n private async shouldStream(\n actions: HandlerResponseWithErrors,\n ): Promise<boolean> {\n const rawProbe = await actions.queryStringWithDefaults(\n \"testing for probe\",\n queryKeys.Probe,\n );\n if (rawProbe !== undefined) {\n return false;\n }\n\n const envStreaming = this.env[envKeys.InngestStreaming];\n if (envStreaming === \"allow\" || envStreaming === \"force\") {\n warnOnce(\n this.client[internalLoggerSymbol],\n \"streaming-allow-force-deprecated\",\n { value: envStreaming },\n `INNGEST_STREAMING=\"${envStreaming}\" is deprecated; set INNGEST_STREAMING=true instead`,\n );\n }\n\n const streamingRequested =\n this.streaming === true ||\n parseAsBoolean(this.env[envKeys.InngestStreaming]) === true ||\n envStreaming === \"allow\" ||\n envStreaming === \"force\";\n\n // We must be able to stream responses to continue.\n if (!actions.transformStreamingResponse) {\n if (streamingRequested) {\n throw new Error(\n `${logPrefix} Streaming has been forced but the serve handler does not support streaming. Please either remove the streaming option or use a serve handler that supports streaming.`,\n );\n }\n return false;\n }\n\n return streamingRequested;\n }\n\n private async isInngestReq(\n actions: HandlerResponseWithErrors,\n ): Promise<boolean> {\n const reqMessage = `checking if this is an Inngest request`;\n\n const [runId, signature] = await Promise.all([\n actions.headers(reqMessage, headerKeys.InngestRunId),\n actions.headers(reqMessage, headerKeys.Signature),\n ]);\n\n // Note that the signature just has to be present; in Dev it'll be empty,\n // but still set to `\"\"`.\n return Boolean(runId && typeof signature === \"string\");\n }\n\n /**\n * Start handling a request, setting up environments, modes, and returning\n * some helpers.\n */\n private async initRequest(...args: Input): Promise<{\n timer: ServerTiming;\n actions: HandlerResponseWithErrors;\n getHeaders: () => Promise<Record<string, string>>;\n }> {\n const timer = new ServerTiming(this.client[internalLoggerSymbol]);\n const actions = await this.getActions(timer, ...args);\n\n const [env, expectedServerKind] = await Promise.all([\n actions.env?.(\"starting to handle request\"),\n actions.headers(\n \"checking expected server kind\",\n headerKeys.InngestServerKind,\n ),\n ]);\n\n // Always make sure to merge whatever env we've been given with\n // `process.env`; some platforms may not provide all the necessary\n // environment variables or may use two sources.\n // Update both handler's env and client's env to ensure consistency.\n this.env = { ...allProcessEnv(), ...env };\n this.client.setEnvVars(this.env);\n\n const headerPromises = forwardedHeaders.map(async (header) => {\n const value = await actions.headers(\n `fetching ${header} for forwarding`,\n header,\n );\n\n return { header, value };\n });\n\n const headersToForwardP = Promise.all(headerPromises).then(\n (fetchedHeaders) => {\n return fetchedHeaders.reduce<Record<string, string>>(\n (acc, { header, value }) => {\n if (value) {\n acc[header] = value;\n }\n\n return acc;\n },\n {},\n );\n },\n );\n\n const getHeaders = async (): Promise<Record<string, string>> => ({\n ...inngestHeaders({\n env: this.env,\n framework: this.frameworkName,\n client: this.client,\n expectedServerKind: expectedServerKind || undefined,\n extras: {\n \"Server-Timing\": timer.getHeader(),\n },\n }),\n ...(await headersToForwardP),\n });\n\n return {\n timer,\n actions,\n getHeaders,\n };\n }\n\n /**\n * `createSyncHandler` should be used to return a type-equivalent version of\n * the `handler` specified during instantiation.\n */\n public createSyncHandler<\n THandler extends (...args: Input) => Promise<Awaited<Output>>,\n >(): (handler: THandler) => THandler {\n // Return a function that can be used to wrap endpoints\n return (handler) => {\n return this.wrapHandler((async (...args) => {\n const reqInit = await this.initRequest(...args);\n\n const fn = new InngestFunction(\n this.client,\n {\n id: this._options.syncOptions?.functionId ?? \"\",\n retries: this._options.syncOptions?.retries ?? defaultMaxRetries,\n },\n () => handler(...args),\n );\n\n // Decide if this request looks like an Inngest request. If it does,\n // we'll just use the regular `serve()` handler for this request, as\n // it's async.\n if (await this.isInngestReq(reqInit.actions)) {\n // If we have a run ID, we can just use the normal serve path\n // return this.createHandler()(...args);\n return this.handleAsyncRequest({\n ...reqInit,\n forceExecution: true,\n args,\n fns: [fn],\n });\n }\n\n // Otherwise, we know this is a sync request, so we can proceed with\n // creating a sync request to Inngest.\n return this.handleSyncRequest({\n ...reqInit,\n args,\n asyncMode:\n this._options.syncOptions?.asyncResponse ??\n AsyncResponseType.Redirect,\n asyncRedirectUrl: this._options.syncOptions?.asyncRedirectUrl,\n fn,\n });\n }) as THandler);\n };\n }\n\n /**\n * `createHandler` should be used to return a type-equivalent version of the\n * `handler` specified during instantiation.\n *\n * @example\n * ```\n * // my-custom-handler.ts\n * import {\n * InngestCommHandler,\n * type ServeHandlerOptions,\n * } from \"./components/InngestCommHandler\";\n *\n * export const serve = (options: ServeHandlerOptions) => {\n * const handler = new InngestCommHandler({\n * frameworkName: \"my-custom-handler\",\n * ...options,\n * handler: (req: Request) => {\n * return {\n * body: () => req.json(),\n * headers: (key) => req.headers.get(key),\n * method: () => req.method,\n * url: () => new URL(req.url, `https://${req.headers.get(\"host\") || \"\"}`),\n * transformResponse: ({ body, status, headers }) => {\n * return new Response(body, { status, headers });\n * },\n * };\n * },\n * });\n *\n * return handler.createHandler();\n * };\n * ```\n */\n public createHandler<\n THandler extends (...args: Input) => Promise<Awaited<Output>>,\n >(): THandler {\n return this.wrapHandler((async (...args) => {\n return this.handleAsyncRequest({\n ...(await this.initRequest(...args)),\n args,\n });\n }) as THandler);\n }\n\n /**\n * Given a set of actions that let us access the incoming request, create an\n * event that repesents a run starting from an HTTP request.\n */\n private async createHttpEvent(\n actions: HandlerResponseWithErrors,\n fn: InngestFunction.Any,\n ): Promise<APIStepPayload> {\n const reason = \"creating sync event\";\n\n const contentTypePromise = actions\n .headers(reason, headerKeys.ContentType)\n .then((v) => v ?? \"\");\n\n const ipPromise = actions\n .headers(reason, headerKeys.ForwardedFor)\n .then((v) => {\n if (v) return v;\n\n return actions.headers(reason, headerKeys.RealIp).then((v) => v ?? \"\");\n });\n\n const methodPromise = actions.method(reason);\n\n const urlPromise = actions.url(reason).then((v) => this.reqUrl(v));\n\n const domainPromise = urlPromise.then(\n (url) => `${url.protocol}//${url.host}`,\n );\n\n const pathPromise = urlPromise.then((url) => url.pathname);\n\n const queryParamsPromise = urlPromise.then((url) =>\n url.searchParams.toString(),\n );\n\n const bodyPromise = actions.body(reason).then((body) => {\n return typeof body === \"string\" ? body : stringify(body);\n });\n\n const [contentType, domain, ip, method, path, queryParams, body] =\n await Promise.all([\n contentTypePromise,\n domainPromise,\n ipPromise,\n methodPromise,\n pathPromise,\n queryParamsPromise,\n bodyPromise,\n ]);\n\n return {\n name: internalEvents.HttpRequest,\n data: {\n content_type: contentType,\n domain,\n ip,\n method,\n path,\n query_params: queryParams,\n body,\n fn: fn.id(),\n },\n };\n }\n\n private async handleSyncRequest({\n timer,\n actions,\n fn,\n asyncMode,\n asyncRedirectUrl,\n args,\n }: {\n timer: ServerTiming;\n actions: HandlerResponseWithErrors;\n fn: InngestFunction.Any;\n asyncMode: AsyncResponseValue;\n asyncRedirectUrl: SyncHandlerOptions[\"asyncRedirectUrl\"];\n args: unknown[];\n }): Promise<Awaited<Output>> {\n // Do we have actions for handling sync requests? We must!\n if (!actions.experimentalTransformSyncResponse) {\n throw new Error(\n \"This platform does not support synchronous Inngest function executions.\",\n );\n }\n\n // Check we're not in a context already...\n const ctx = await getAsyncCtx();\n if (ctx) {\n throw new Error(\n \"We already seem to be in the context of an Inngest execution, but didn't expect to be. Did you already wrap this handler?\",\n );\n }\n\n // We create a new run ID here in the SDK.\n const { ulid } = await import(\"ulid\"); // lazy loading for edge envs\n const runId = ulid();\n const event = await this.createHttpEvent(actions, fn);\n\n const exeVersion = ExecutionVersion.V2;\n\n const exe = fn[\"createExecution\"]({\n partialOptions: {\n client: this.client,\n data: {\n runId,\n event,\n attempt: 0,\n events: [event],\n maxAttempts: fn.opts.retries ?? defaultMaxRetries,\n },\n runId,\n headers: {},\n reqArgs: args,\n stepCompletionOrder: [],\n stepState: {},\n disableImmediateExecution: false,\n isFailureHandler: false,\n timer,\n createResponse: (data: unknown) =>\n actions.experimentalTransformSyncResponse!(\n \"creating sync execution\",\n data,\n ).then((res) => ({\n ...res,\n version: exeVersion,\n })),\n stepMode: StepMode.Sync,\n },\n });\n\n const result = await exe.start();\n\n const resultHandlers: ExecutionResultHandlers<unknown> = {\n \"step-not-found\": () => {\n throw new Error(\n \"We should not get the result 'step-not-found' when checkpointing. This is a bug in the `inngest` SDK\",\n );\n },\n \"steps-found\": () => {\n throw new Error(\n \"We should not get the result 'steps-found' when checkpointing. This is a bug in the `inngest` SDK\",\n );\n },\n \"step-ran\": () => {\n throw new Error(\n \"We should not get the result 'step-ran' when checkpointing. This is a bug in the `inngest` SDK\",\n );\n },\n \"function-rejected\": (result) => {\n return actions.transformResponse(\"creating sync error response\", {\n status: result.retriable ? 500 : 400,\n headers: {\n \"Content-Type\": \"application/json\",\n [headerKeys.NoRetry]: result.retriable ? \"false\" : \"true\",\n ...(typeof result.retriable === \"string\"\n ? { [headerKeys.RetryAfter]: result.retriable }\n : {}),\n },\n version: exeVersion,\n body: stringify(undefinedToNull(result.error)),\n });\n },\n \"function-resolved\": ({ data }) => {\n // We're done and we didn't call any step tools, so just return the\n // response.\n return data;\n },\n \"change-mode\": async ({ token }) => {\n switch (asyncMode) {\n case AsyncResponseType.Redirect: {\n let redirectUrl: string;\n\n if (asyncRedirectUrl) {\n if (typeof asyncRedirectUrl === \"function\") {\n // Full control: user provides complete URL\n redirectUrl = await asyncRedirectUrl({ runId, token });\n } else {\n // String path: resolve relative to request origin\n // new URL(\"/api/poll\", \"https://example.com\") → \"https://example.com/api/poll\"\n // new URL(\"https://other.com/poll\", \"https://example.com\") → \"https://other.com/poll\"\n const baseUrl = await actions.url(\"getting request origin\");\n const url = new URL(asyncRedirectUrl, baseUrl.origin);\n url.searchParams.set(\"runId\", runId);\n url.searchParams.set(\"token\", token);\n redirectUrl = url.toString();\n }\n } else {\n // Default: redirect to Inngest API\n redirectUrl = await this.client[\"inngestApi\"]\n [\"getTargetUrl\"](`/v1/http/runs/${runId}/output?token=${token}`)\n .then((url) => url.toString());\n }\n\n return actions.transformResponse(\n \"creating sync->async redirect response\",\n {\n status: 302,\n headers: {\n [headerKeys.Location]: redirectUrl,\n },\n version: exeVersion,\n body: \"\",\n },\n );\n }\n\n case AsyncResponseType.Token: {\n return actions.transformResponse(\n \"creating sync->async token response\",\n {\n status: 200,\n headers: {},\n version: exeVersion,\n body: stringify({ run_id: runId, token }),\n },\n );\n }\n\n default: {\n // TODO user-provided hook mate, incl. req args\n break;\n }\n }\n\n throw new Error(\"Not implemented: change-mode\");\n },\n };\n\n const resultHandler = resultHandlers[\n result.type\n ] as ExecutionResultHandler<unknown>;\n if (!resultHandler) {\n throw new Error(\n `No handler for execution result type: ${result.type}. This is a bug in the \\`inngest\\` SDK`,\n );\n }\n\n return resultHandler(result) as Awaited<Output>;\n }\n\n private async handleAsyncRequest({\n timer,\n actions,\n args,\n getHeaders,\n forceExecution,\n fns,\n }: {\n timer: ServerTiming;\n actions: HandlerResponseWithErrors;\n args: Input;\n getHeaders: () => Promise<Record<string, string>>;\n forceExecution?: boolean;\n fns?: InngestFunction.Any[];\n }): Promise<Awaited<Output>> {\n if (forceExecution && !actions.experimentalTransformSyncResponse) {\n throw new Error(\n \"This platform does not support async executions in Inngest for APIs.\",\n );\n }\n\n const methodP = actions.method(\"starting to handle request\");\n\n const [signature, method, body] = await Promise.all([\n actions\n .headers(\"checking signature for request\", headerKeys.Signature)\n .then((headerSignature) => {\n return headerSignature ?? undefined;\n }),\n methodP,\n methodP.then(async (method) => {\n if (method === \"POST\" || method === \"PUT\") {\n const body = await actions.body(\n `checking body for request signing as method is ${method}`,\n );\n if (!body) {\n // Empty body can happen with PUT requests\n return \"\";\n }\n // Some adapters return strings (req.text()), others return\n // pre-parsed objects (req.body). Handle both cases.\n if (typeof body === \"string\") {\n return JSON.parse(body);\n }\n return body;\n }\n\n return \"\";\n }),\n ]);\n\n const signatureValidation = this.validateSignature(signature, body);\n\n // Create middleware instances once; shared by wrapRequest and execution hooks.\n // Starts with client-level middleware; function-level middleware is appended\n // for POST requests once the target function is known.\n const mwInstances = this.client.middleware.map(\n (Cls) => new Cls({ client: this.client }),\n );\n\n /**\n * Prepares an action response by merging returned data to provide\n * trailing information such as `Server-Timing` headers.\n *\n * It should always prioritize the headers returned by the action, as they\n * may contain important information such as `Content-Type`.\n */\n const prepareActionRes = async (\n res: ActionResponse,\n ): Promise<ActionResponse> => {\n const headers: Record<string, string> = {\n ...(await getHeaders()),\n ...res.headers,\n ...(res.version === null\n ? {}\n : {\n [headerKeys.RequestVersion]: (\n res.version ?? PREFERRED_ASYNC_EXECUTION_VERSION\n ).toString(),\n }),\n };\n\n let signature: string | undefined;\n\n try {\n signature = await signatureValidation.then(async (result) => {\n if (!result.success || !result.keyUsed) {\n return undefined;\n }\n\n return await this.getResponseSignature(result.keyUsed, res.body);\n });\n } catch (err) {\n // If we fail to sign, retun a 500 with the error.\n return {\n ...res,\n headers,\n body: stringify(serializeError(err)),\n status: 500,\n };\n }\n\n if (signature) {\n headers[headerKeys.Signature] = signature;\n }\n\n return {\n ...res,\n headers,\n };\n };\n\n // Build the inner handler that wraps handleAction + prepareActionRes.\n // We capture `version` via closure so it can be passed to transformResponse.\n let actionResponseVersion: ExecutionVersion | null | undefined;\n\n const handleAndPrepare = async (): Promise<ActionResponse> => {\n const rawRes = await timer.wrap(\"action\", () =>\n this.handleAction({\n actions,\n timer,\n getHeaders,\n reqArgs: args,\n signatureValidation,\n body,\n method,\n forceExecution: Boolean(forceExecution),\n fns,\n mwInstances,\n }),\n );\n actionResponseVersion = rawRes.version;\n return prepareActionRes(rawRes);\n };\n\n // Only wrap POST requests with the wrapRequest middleware chain.\n // GET/PUT (introspection, registration) bypass the middleware.\n let chainResult: Promise<Middleware.Response>;\n if (method === \"POST\") {\n const url = await actions.url(\"building requestInfo for middleware\");\n\n // Append function-level middleware so it is scoped to this function only.\n const fnId = url.searchParams.get(queryKeys.FnId);\n const matchedFn = fnId ? this.fns[fnId] : undefined;\n const fnMw = matchedFn?.fn?.opts?.middleware ?? [];\n mwInstances.push(\n ...fnMw.map((Cls) => {\n return new Cls({ client: this.client });\n }),\n );\n\n const fn = matchedFn?.fn ?? null;\n\n const requestInfo: Middleware.Request = {\n headers: Object.freeze({ ...(await getHeaders()) }),\n method,\n url,\n body: () => Promise.resolve(body),\n };\n\n let runId = \"\";\n if (\n isRecord(body) &&\n isRecord(body.ctx) &&\n body.ctx.run_id &&\n typeof body.ctx.run_id === \"string\"\n ) {\n runId = body.ctx.run_id;\n }\n\n const innerHandler = async (): Promise<Middleware.Response> => {\n const prepared = await handleAndPrepare();\n return {\n status: prepared.status,\n headers: prepared.headers,\n body: prepared.body,\n };\n };\n\n const wrappedHandler = buildWrapRequestChain({\n fn,\n handler: innerHandler,\n middleware: mwInstances,\n requestArgs: args,\n requestInfo,\n runId,\n });\n\n // Start eagerly (matches prior behavior where handleAction starts before\n // the shouldStream check).\n chainResult = wrappedHandler();\n } else {\n chainResult = handleAndPrepare().then((prepared) => ({\n status: prepared.status,\n headers: prepared.headers,\n body: prepared.body,\n }));\n }\n\n // Attach error handling: if wrapRequest middleware throws, convert to 500.\n const safeChainResult = chainResult.catch(\n (err): Middleware.Response => ({\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n body: stringify({\n type: \"internal\",\n ...serializeError(err as Error),\n }),\n }),\n );\n\n let shouldStream: boolean;\n try {\n shouldStream = await this.shouldStream(actions);\n } catch (err) {\n return actions.transformResponse(\"sending back response\", {\n status: 500,\n headers: {\n ...(await getHeaders()),\n \"Content-Type\": \"application/json\",\n },\n body: stringify(serializeError(err)),\n version: undefined,\n });\n }\n\n if (shouldStream) {\n const method = await actions.method(\"starting streaming response\");\n\n if (method === \"POST\") {\n const { stream, finalize } = await createStream();\n\n /**\n * Errors are handled by `handleAction` here to ensure that an\n * appropriate response is always given.\n */\n void safeChainResult.then((res) => {\n return finalize(\n Promise.resolve({\n ...res,\n version: actionResponseVersion,\n }),\n );\n });\n\n return timer.wrap(\"res\", async () => {\n return actions.transformStreamingResponse?.(\n \"starting streaming response\",\n {\n status: 201,\n headers: await getHeaders(),\n body: stream,\n version: null,\n },\n );\n });\n }\n }\n\n return timer.wrap(\"res\", async () => {\n return safeChainResult.then((res) => {\n return actions.transformResponse(\"sending back response\", {\n ...res,\n version: actionResponseVersion,\n });\n });\n });\n }\n\n private async getActions(\n timer: ServerTiming,\n ...args: Input\n ): Promise<HandlerResponseWithErrors> {\n /**\n * Used for testing, allow setting action overrides externally when\n * calling the handler. Always search the final argument.\n */\n const lastArg = args[args.length - 1] as unknown;\n const actionOverrides =\n typeof lastArg === \"object\" &&\n lastArg !== null &&\n \"actionOverrides\" in lastArg &&\n typeof lastArg[\"actionOverrides\"] === \"object\" &&\n lastArg[\"actionOverrides\"] !== null\n ? lastArg[\"actionOverrides\"]\n : {};\n\n /**\n * We purposefully `await` the handler, as it could be either sync or\n * async.\n */\n const rawActions = {\n ...(await timer\n .wrap(\"handler\", () => this.handler(...args))\n .catch(rethrowError(\"Serve handler failed to run\"))),\n ...actionOverrides,\n };\n\n /**\n * Map over every `action` in `rawActions` and create a new `actions`\n * object where each function is safely promisified with each access\n * requiring a reason.\n *\n * This helps us provide high quality errors about what's going wrong for\n * each access without having to wrap every access in a try/catch.\n */\n const promisifiedActions: ActionHandlerResponseWithErrors = Object.entries(\n rawActions,\n ).reduce((acc, [key, value]) => {\n if (typeof value !== \"function\") {\n return acc;\n }\n\n return {\n ...acc,\n [key]: (reason: string, ...args: unknown[]) => {\n const errMessage = [\n `Failed calling \\`${key}\\` from serve handler`,\n reason,\n ]\n .filter(Boolean)\n .join(\" when \");\n\n const fn = () => (value as (...args: unknown[]) => unknown)(...args);\n\n return runAsPromise(fn)\n .catch(rethrowError(errMessage))\n .catch((err) => {\n this.client[internalLoggerSymbol].error({ err }, errMessage);\n throw err;\n });\n },\n };\n }, {} as ActionHandlerResponseWithErrors);\n\n /**\n * Mapped promisified handlers from userland `serve()` function mixed in\n * with some helpers.\n */\n const actions: HandlerResponseWithErrors = {\n ...promisifiedActions,\n queryStringWithDefaults: async (\n reason: string,\n key: string,\n ): Promise<string | undefined> => {\n const url = await actions.url(reason);\n\n const ret =\n (await actions.queryString?.(reason, key, url)) ||\n url.searchParams.get(key) ||\n undefined;\n\n return ret;\n },\n ...actionOverrides,\n };\n\n return actions;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: any fn\n private wrapHandler<THandler extends (...args: any[]) => any>(\n handler: THandler,\n ): THandler {\n /**\n * Some platforms check (at runtime) the length of the function being used\n * to handle an endpoint. If this is a variadic function, it will fail that\n * check.\n *\n * Therefore, we expect the arguments accepted to be the same length as the\n * `handler` function passed internally.\n *\n * We also set a name to avoid a common useless name in tracing such as\n * `\"anonymous\"` or `\"bound function\"`.\n *\n * https://github.com/getsentry/sentry-javascript/issues/3284\n */\n Object.defineProperties(handler, {\n name: {\n value: \"InngestHandler\",\n },\n length: {\n value: this.handler.length,\n },\n });\n\n return handler;\n }\n\n /**\n * Given a set of functions to check if an action is available from the\n * instance's handler, enact any action that is found.\n *\n * This method can fetch varying payloads of data, but ultimately is the place\n * where _decisions_ are made regarding functionality.\n *\n * For example, if we find that we should be viewing the UI, this function\n * will decide whether the UI should be visible based on the payload it has\n * found (e.g. env vars, options, etc).\n */\n private async handleAction({\n actions,\n timer,\n getHeaders,\n reqArgs,\n signatureValidation,\n body: rawBody,\n method,\n forceExecution,\n fns,\n mwInstances,\n }: {\n actions: HandlerResponseWithErrors;\n timer: ServerTiming;\n getHeaders: () => Promise<Record<string, string>>;\n reqArgs: unknown[];\n signatureValidation: ReturnType<InngestCommHandler[\"validateSignature\"]>;\n body: unknown;\n method: string;\n forceExecution: boolean;\n fns?: InngestFunction.Any[];\n mwInstances?: Middleware.BaseMiddleware[];\n }): Promise<ActionResponse> {\n if (!this.checkModeConfiguration()) {\n return internalServerErrorResponse;\n }\n\n // This is when the request body is completely missing. This commonly\n // happens when the HTTP framework doesn't have body parsing middleware,\n // or for PUT requests that don't require a body.\n const isMissingBody = !rawBody;\n let body = rawBody;\n\n try {\n let url = await actions.url(\"starting to handle request\");\n\n if (method === \"POST\" || forceExecution) {\n if (!forceExecution && isMissingBody) {\n this.client[internalLoggerSymbol].error(\n \"Missing body when executing, possibly du