@genkit-ai/express 1.35.0 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -60,9 +60,10 @@ function expressHandler(action, opts) {
60
60
  context = await opts?.contextProvider?.({
61
61
  method: request.method,
62
62
  headers: Object.fromEntries(
63
- Object.entries(request.headers).map(([key, value]) => [
63
+ Object.entries(request.headers).filter(([, value]) => value !== void 0).map(([key, value]) => [
64
64
  key.toLowerCase(),
65
- Array.isArray(value) ? value.join(" ") : String(value)
65
+ // RFC 9110 5.3: combine repeated field lines with a comma.
66
+ Array.isArray(value) ? value.join(", ") : String(value)
66
67
  ])
67
68
  ),
68
69
  input
@@ -82,7 +83,7 @@ ${e.stack}`
82
83
  request.on("timeout", () => {
83
84
  abortController.abort();
84
85
  });
85
- if (request.get("Accept") === "text/event-stream" || stream === "true") {
86
+ if (request.get("Accept")?.toLowerCase().includes("text/event-stream") || stream === "true") {
86
87
  const streamManager = opts?.streamManager;
87
88
  if (streamManager && streamId) {
88
89
  await subscribeToStream(streamManager, streamId, response);
@@ -136,6 +137,7 @@ async function runActionWithDurableStreaming(action, streamManager, streamId, in
136
137
  }
137
138
  try {
138
139
  let onChunk = (chunk) => {
140
+ if (response.destroyed) return;
139
141
  response.write(
140
142
  "data: " + JSON.stringify({ message: chunk }) + streamDelimiter
141
143
  );
@@ -156,10 +158,12 @@ async function runActionWithDurableStreaming(action, streamManager, streamId, in
156
158
  taskQueue.enqueue(() => durableStream.done(result.result));
157
159
  await taskQueue.merge();
158
160
  }
159
- response.write(
160
- "data: " + JSON.stringify({ result: result.result }) + streamDelimiter
161
- );
162
- response.end();
161
+ if (!response.destroyed) {
162
+ response.write(
163
+ "data: " + JSON.stringify({ result: result.result }) + streamDelimiter
164
+ );
165
+ response.end();
166
+ }
163
167
  } catch (e) {
164
168
  if (durableStream) {
165
169
  taskQueue.enqueue(() => durableStream.error(e));
@@ -169,23 +173,27 @@ async function runActionWithDurableStreaming(action, streamManager, streamId, in
169
173
  `Streaming request failed with error: ${e.message}
170
174
  ${e.stack}`
171
175
  );
172
- response.write(
173
- `error: ${JSON.stringify({
174
- error: (0, import_context.getCallableJSON)(e)
175
- })}${streamDelimiter}`
176
- );
177
- response.end();
176
+ if (!response.destroyed) {
177
+ response.write(
178
+ `error: ${JSON.stringify({
179
+ error: (0, import_context.getCallableJSON)(e)
180
+ })}${streamDelimiter}`
181
+ );
182
+ response.end();
183
+ }
178
184
  }
179
185
  }
180
186
  async function subscribeToStream(streamManager, streamId, response) {
181
187
  try {
182
188
  await streamManager.subscribe(streamId, {
183
189
  onChunk: (chunk) => {
190
+ if (response.destroyed) return;
184
191
  response.write(
185
192
  "data: " + JSON.stringify({ message: chunk }) + streamDelimiter
186
193
  );
187
194
  },
188
195
  onDone: (output) => {
196
+ if (response.destroyed) return;
189
197
  response.write(
190
198
  "data: " + JSON.stringify({ result: output }) + streamDelimiter
191
199
  );
@@ -196,6 +204,7 @@ async function subscribeToStream(streamManager, streamId, response) {
196
204
  `Streaming request failed with error: ${err.message}
197
205
  ${err.stack}`
198
206
  );
207
+ if (response.destroyed) return;
199
208
  response.write(
200
209
  `error: ${JSON.stringify({
201
210
  error: (0, import_context.getCallableJSON)(err)
@@ -205,6 +214,7 @@ ${err.stack}`
205
214
  }
206
215
  });
207
216
  } catch (e) {
217
+ if (response.destroyed) return;
208
218
  if (e instanceof import_beta.StreamNotFoundError) {
209
219
  response.status(204).end();
210
220
  return;
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { type CorsOptions } from 'cors';\nimport { randomUUID } from 'crypto';\nimport express from 'express';\nimport {\n Action,\n ActionStreamInput,\n AsyncTaskQueue,\n Flow,\n StreamNotFoundError,\n type ActionContext,\n type StreamManager,\n type z,\n} from 'genkit/beta';\nimport {\n getCallableJSON,\n getHttpStatus,\n type ContextProvider,\n type RequestData,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport type { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n streamManager?: StreamManager;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n const streamIdHeader = request.headers['x-genkit-stream-id'];\n const streamId = Array.isArray(streamIdHeader)\n ? streamIdHeader[0]\n : streamIdHeader;\n\n if (!request.body) {\n const errMsg =\n `Error: request.body is undefined. ` +\n `Possible reasons: missing 'content-type: application/json' in request ` +\n `headers or misconfigured JSON middleware ('app.use(express.json()')? `;\n logger.error(errMsg);\n response\n .status(400)\n .json({ message: errMsg, status: 'INVALID ARGUMENT' })\n .end();\n return;\n }\n\n const input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [\n key.toLowerCase(),\n Array.isArray(value) ? value.join(' ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n const abortController = new AbortController();\n request.on('close', () => {\n abortController.abort();\n });\n // when/if using timeout middleware, it will emit 'timeout' event.\n request.on('timeout', () => {\n abortController.abort();\n });\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n const streamManager = opts?.streamManager;\n if (streamManager && streamId) {\n await subscribeToStream(streamManager, streamId, response);\n return;\n }\n const streamIdToUse = randomUUID();\n const headers = {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n };\n if (streamManager) {\n headers['x-genkit-stream-id'] = streamIdToUse;\n }\n response.writeHead(200, headers);\n runActionWithDurableStreaming(\n action,\n streamManager,\n streamIdToUse,\n input,\n context,\n response,\n abortController.signal\n );\n } else {\n try {\n const result = await action.run(input, {\n context,\n abortSignal: abortController.signal,\n });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\nasync function runActionWithDurableStreaming<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n streamManager: StreamManager | undefined,\n streamId: string,\n input: z.infer<I>,\n context: ActionContext,\n response: express.Response,\n abortSignal: AbortSignal\n) {\n let taskQueue: AsyncTaskQueue | undefined;\n let durableStream: ActionStreamInput<any, any> | undefined;\n if (streamManager) {\n taskQueue = new AsyncTaskQueue();\n durableStream = await streamManager.open(streamId);\n }\n try {\n let onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n if (streamManager) {\n const originalOnChunk = onChunk;\n onChunk = (chunk: z.infer<S>) => {\n originalOnChunk(chunk);\n taskQueue!.enqueue(() => durableStream!.write(chunk));\n };\n }\n const result = await action.run(input, {\n onChunk,\n context,\n abortSignal,\n });\n if (streamManager) {\n taskQueue!.enqueue(() => durableStream!.done(result.result));\n await taskQueue!.merge();\n }\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n if (durableStream) {\n taskQueue!.enqueue(() => durableStream!.error(e));\n await taskQueue!.merge();\n }\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${\n (e as Error).stack\n }`\n );\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n }\n}\n\nasync function subscribeToStream(\n streamManager: StreamManager,\n streamId: string,\n response: express.Response\n): Promise<void> {\n try {\n await streamManager.subscribe(streamId, {\n onChunk: (chunk) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n },\n onDone: (output) => {\n response.write(\n 'data: ' + JSON.stringify({ result: output }) + streamDelimiter\n );\n response.end();\n },\n onError: (err) => {\n logger.error(\n `Streaming request failed with error: ${(err as Error).message}\\n${\n (err as Error).stack\n }`\n );\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(err),\n })}${streamDelimiter}`\n );\n response.end();\n },\n });\n } catch (e: any) {\n if (e instanceof StreamNotFoundError) {\n response.status(204).end();\n return;\n }\n if (e.status === 'DEADLINE_EXCEEDED') {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n return;\n }\n throw e;\n }\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * A wrapper object containing a flow with its associated options.\n */\nexport type FlowWithOptions<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n };\n};\n\n/**\n * Adds an auth policy to the flow.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withFlowOptions<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n }\n): FlowWithOptions<I, O, S> {\n return {\n flow,\n options,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (\n | Flow<any, any, any>\n | FlowWithContextProvider<any, any, any>\n | FlowWithOptions<any, any, any>\n )[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('flow' in flow) {\n const flowPath = `/${pathPrefix}${\n ('options' in flow && flow.options.path) || flow.flow.__action.name\n }`;\n logger.debug(` - ${flowPath}`);\n const options =\n 'options' in flow ? flow.options : { contextProvider: flow.context };\n server.post(flowPath, expressHandler(flow.flow, options));\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? Number.parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,yBAAuB;AACvB,kBAAuC;AACvC,oBAA2B;AAC3B,qBAAoB;AACpB,kBASO;AACP,qBAKO;AACP,qBAAuB;AAGvB,MAAM,kBAAkB;AAKjB,SAAS,eAMd,QACA,MAIwB;AACxB,SAAO,OACL,SACA,aACkB;AAClB,UAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,UAAM,iBAAiB,QAAQ,QAAQ,oBAAoB;AAC3D,UAAM,WAAW,MAAM,QAAQ,cAAc,IACzC,eAAe,CAAC,IAChB;AAEJ,QAAI,CAAC,QAAQ,MAAM;AACjB,YAAM,SACJ;AAGF,4BAAO,MAAM,MAAM;AACnB,eACG,OAAO,GAAG,EACV,KAAK,EAAE,SAAS,QAAQ,QAAQ,mBAAmB,CAAC,EACpD,IAAI;AACP;AAAA,IACF;AAEA,UAAM,QAAQ,QAAQ,KAAK;AAC3B,QAAI;AAEJ,QAAI;AACF,gBACG,MAAM,MAAM,kBAAkB;AAAA,QAC7B,QAAQ,QAAQ;AAAA,QAChB,SAAS,OAAO;AAAA,UACd,OAAO,QAAQ,QAAQ,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM;AAAA,YACpD,IAAI,YAAY;AAAA,YAChB,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,GAAG,IAAI,OAAO,KAAK;AAAA,UACvD,CAAC;AAAA,QACH;AAAA,QACA;AAAA,MACF,CAAC,KAAM,CAAC;AAAA,IACZ,SAAS,GAAQ;AACf,4BAAO;AAAA,QACL,kCAAmC,EAAY,OAAO;AAAA,EAAM,EAAY,KAAK;AAAA,MAC/E;AACA,eAAS,WAAO,8BAAc,CAAC,CAAC,EAAE,SAAK,gCAAgB,CAAC,CAAC,EAAE,IAAI;AAC/D;AAAA,IACF;AAEA,UAAM,kBAAkB,IAAI,gBAAgB;AAC5C,YAAQ,GAAG,SAAS,MAAM;AACxB,sBAAgB,MAAM;AAAA,IACxB,CAAC;AAED,YAAQ,GAAG,WAAW,MAAM;AAC1B,sBAAgB,MAAM;AAAA,IACxB,CAAC;AAED,QAAI,QAAQ,IAAI,QAAQ,MAAM,uBAAuB,WAAW,QAAQ;AACtE,YAAM,gBAAgB,MAAM;AAC5B,UAAI,iBAAiB,UAAU;AAC7B,cAAM,kBAAkB,eAAe,UAAU,QAAQ;AACzD;AAAA,MACF;AACA,YAAM,oBAAgB,0BAAW;AACjC,YAAM,UAAU;AAAA,QACd,gBAAgB;AAAA,QAChB,qBAAqB;AAAA,MACvB;AACA,UAAI,eAAe;AACjB,gBAAQ,oBAAoB,IAAI;AAAA,MAClC;AACA,eAAS,UAAU,KAAK,OAAO;AAC/B;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,gBAAgB;AAAA,MAClB;AAAA,IACF,OAAO;AACL,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,IAAI,OAAO;AAAA,UACrC;AAAA,UACA,aAAa,gBAAgB;AAAA,QAC/B,CAAC;AACD,iBAAS,UAAU,qBAAqB,OAAO,UAAU,OAAO;AAChE,iBAAS,UAAU,oBAAoB,OAAO,UAAU,MAAM;AAE9D,iBACG,OAAO,GAAG,EACV,KAAK;AAAA,UACJ,QAAQ,OAAO;AAAA,QACjB,CAAC,EACA,IAAI;AAAA,MACT,SAAS,GAAG;AAEV,8BAAO;AAAA,UACL,4CAA6C,EAAY,OAAO;AAAA,EAAM,EAAY,KAAK;AAAA,QACzF;AACA,iBAAS,WAAO,8BAAc,CAAC,CAAC,EAAE,SAAK,gCAAgB,CAAC,CAAC,EAAE,IAAI;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAe,8BAKb,QACA,eACA,UACA,OACA,SACA,UACA,aACA;AACA,MAAI;AACJ,MAAI;AACJ,MAAI,eAAe;AACjB,gBAAY,IAAI,2BAAe;AAC/B,oBAAgB,MAAM,cAAc,KAAK,QAAQ;AAAA,EACnD;AACA,MAAI;AACF,QAAI,UAAU,CAAC,UAAsB;AACnC,eAAS;AAAA,QACP,WAAW,KAAK,UAAU,EAAE,SAAS,MAAM,CAAC,IAAI;AAAA,MAClD;AAAA,IACF;AACA,QAAI,eAAe;AACjB,YAAM,kBAAkB;AACxB,gBAAU,CAAC,UAAsB;AAC/B,wBAAgB,KAAK;AACrB,kBAAW,QAAQ,MAAM,cAAe,MAAM,KAAK,CAAC;AAAA,MACtD;AAAA,IACF;AACA,UAAM,SAAS,MAAM,OAAO,IAAI,OAAO;AAAA,MACrC;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,QAAI,eAAe;AACjB,gBAAW,QAAQ,MAAM,cAAe,KAAK,OAAO,MAAM,CAAC;AAC3D,YAAM,UAAW,MAAM;AAAA,IACzB;AACA,aAAS;AAAA,MACP,WAAW,KAAK,UAAU,EAAE,QAAQ,OAAO,OAAO,CAAC,IAAI;AAAA,IACzD;AACA,aAAS,IAAI;AAAA,EACf,SAAS,GAAG;AACV,QAAI,eAAe;AACjB,gBAAW,QAAQ,MAAM,cAAe,MAAM,CAAC,CAAC;AAChD,YAAM,UAAW,MAAM;AAAA,IACzB;AACA,0BAAO;AAAA,MACL,wCAAyC,EAAY,OAAO;AAAA,EACzD,EAAY,KACf;AAAA,IACF;AACA,aAAS;AAAA,MACP,UAAU,KAAK,UAAU;AAAA,QACvB,WAAO,gCAAgB,CAAC;AAAA,MAC1B,CAAC,CAAC,GAAG,eAAe;AAAA,IACtB;AACA,aAAS,IAAI;AAAA,EACf;AACF;AAEA,eAAe,kBACb,eACA,UACA,UACe;AACf,MAAI;AACF,UAAM,cAAc,UAAU,UAAU;AAAA,MACtC,SAAS,CAAC,UAAU;AAClB,iBAAS;AAAA,UACP,WAAW,KAAK,UAAU,EAAE,SAAS,MAAM,CAAC,IAAI;AAAA,QAClD;AAAA,MACF;AAAA,MACA,QAAQ,CAAC,WAAW;AAClB,iBAAS;AAAA,UACP,WAAW,KAAK,UAAU,EAAE,QAAQ,OAAO,CAAC,IAAI;AAAA,QAClD;AACA,iBAAS,IAAI;AAAA,MACf;AAAA,MACA,SAAS,CAAC,QAAQ;AAChB,8BAAO;AAAA,UACL,wCAAyC,IAAc,OAAO;AAAA,EAC3D,IAAc,KACjB;AAAA,QACF;AACA,iBAAS;AAAA,UACP,UAAU,KAAK,UAAU;AAAA,YACvB,WAAO,gCAAgB,GAAG;AAAA,UAC5B,CAAC,CAAC,GAAG,eAAe;AAAA,QACtB;AACA,iBAAS,IAAI;AAAA,MACf;AAAA,IACF,CAAC;AAAA,EACH,SAAS,GAAQ;AACf,QAAI,aAAa,iCAAqB;AACpC,eAAS,OAAO,GAAG,EAAE,IAAI;AACzB;AAAA,IACF;AACA,QAAI,EAAE,WAAW,qBAAqB;AACpC,eAAS;AAAA,QACP,UAAU,KAAK,UAAU;AAAA,UACvB,WAAO,gCAAgB,CAAC;AAAA,QAC1B,CAAC,CAAC,GAAG,eAAe;AAAA,MACtB;AACA,eAAS,IAAI;AACb;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;AAoCO,SAAS,oBAMd,MACA,SACqC;AACrC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;AAKO,SAAS,gBAKd,MACA,SAK0B;AAC1B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;AAyBO,SAAS,gBAAgB,SAAwC;AACtE,QAAM,SAAS,IAAI,WAAW,OAAO;AACrC,SAAO,MAAM;AACb,SAAO;AACT;AASO,MAAM,WAAW;AAAA;AAAA,EAEtB,OAAe,kBAAgC,CAAC;AAAA;AAAA,EAGxC;AAAA;AAAA,EAEA,OAAsB;AAAA;AAAA,EAEtB,SAAwB;AAAA,EAEhC,YAAY,SAA4B;AACtC,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,IACL;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAQ;AACZ,UAAM,aAAS,eAAAA,SAAQ;AAEvB,WAAO,IAAI,mBAAAC,QAAW,KAAK,KAAK,QAAQ,iBAAiB,CAAC;AAC1D,WAAO,QAAI,YAAAC,SAAK,KAAK,QAAQ,IAAI,CAAC;AAElC,0BAAO,MAAM,sCAAsC;AACnD,UAAM,aAAa,KAAK,QAAQ,cAAc;AAC9C,SAAK,QAAQ,OAAO,QAAQ,CAAC,SAAS;AACpC,UAAI,UAAU,MAAM;AAClB,cAAM,WAAW,IAAI,UAAU,GAC5B,aAAa,QAAQ,KAAK,QAAQ,QAAS,KAAK,KAAK,SAAS,IACjE;AACA,8BAAO,MAAM,MAAM,QAAQ,EAAE;AAC7B,cAAM,UACJ,aAAa,OAAO,KAAK,UAAU,EAAE,iBAAiB,KAAK,QAAQ;AACrE,eAAO,KAAK,UAAU,eAAe,KAAK,MAAM,OAAO,CAAC;AAAA,MAC1D,OAAO;AACL,cAAM,WAAW,IAAI,UAAU,GAAG,KAAK,SAAS,IAAI;AACpD,8BAAO,MAAM,MAAM,QAAQ,EAAE;AAC7B,eAAO,KAAK,UAAU,eAAe,IAAI,CAAC;AAAA,MAC5C;AAAA,IACF,CAAC;AACD,SAAK,OACH,KAAK,SAAS,SACb,QAAQ,IAAI,OAAO,OAAO,SAAS,QAAQ,IAAI,IAAI,IAAI,MACxD;AACF,SAAK,SAAS,OAAO,OAAO,KAAK,MAAM,MAAM;AAC3C,4BAAO,MAAM,2CAA2C,KAAK,IAAI,EAAE;AACnE,iBAAW,gBAAgB,KAAK,IAAI;AAAA,IACtC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC1B,QAAI,CAAC,KAAK,QAAQ;AAChB;AAAA,IACF;AACA,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,WAAK,OAAQ,MAAM,CAAC,QAAQ;AAC1B,YAAI,KAAK;AACP,gCAAO;AAAA,YACL,2CAA2C,KAAK,IAAI,KAAK,GAAG;AAAA,UAC9D;AACA,iBAAO,GAAG;AAAA,QACZ;AACA,cAAM,QAAQ,WAAW,gBAAgB,QAAQ,IAAI;AACrD,YAAI,QAAQ,IAAI;AACd,qBAAW,gBAAgB,OAAO,OAAO,CAAC;AAAA,QAC5C;AACA,8BAAO;AAAA,UACL,uBAAuB,KAAK,IAAI;AAAA,QAClC;AACA,aAAK,OAAO;AACZ,aAAK,SAAS;AACd,gBAAQ;AAAA,MACV,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,UAAU;AACrB,WAAO,QAAQ;AAAA,MACb,WAAW,gBAAgB,IAAI,CAAC,WAAW,OAAO,KAAK,CAAC;AAAA,IAC1D;AAAA,EACF;AACF;","names":["express","bodyParser","cors"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { type CorsOptions } from 'cors';\nimport { randomUUID } from 'crypto';\nimport express from 'express';\nimport {\n Action,\n ActionStreamInput,\n AsyncTaskQueue,\n Flow,\n StreamNotFoundError,\n type ActionContext,\n type StreamManager,\n type z,\n} from 'genkit/beta';\nimport {\n getCallableJSON,\n getHttpStatus,\n type ContextProvider,\n type RequestData,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport type { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n streamManager?: StreamManager;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n const streamIdHeader = request.headers['x-genkit-stream-id'];\n const streamId = Array.isArray(streamIdHeader)\n ? streamIdHeader[0]\n : streamIdHeader;\n\n if (!request.body) {\n const errMsg =\n `Error: request.body is undefined. ` +\n `Possible reasons: missing 'content-type: application/json' in request ` +\n `headers or misconfigured JSON middleware ('app.use(express.json()')? `;\n logger.error(errMsg);\n response\n .status(400)\n .json({ message: errMsg, status: 'INVALID ARGUMENT' })\n .end();\n return;\n }\n\n const input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers)\n // Skip headers explicitly set to undefined so they don't become\n // the literal string \"undefined\" via String(value).\n .filter(([, value]) => value !== undefined)\n .map(([key, value]) => [\n key.toLowerCase(),\n // RFC 9110 5.3: combine repeated field lines with a comma.\n Array.isArray(value) ? value.join(', ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n const abortController = new AbortController();\n request.on('close', () => {\n abortController.abort();\n });\n // when/if using timeout middleware, it will emit 'timeout' event.\n request.on('timeout', () => {\n abortController.abort();\n });\n\n if (\n request.get('Accept')?.toLowerCase().includes('text/event-stream') ||\n stream === 'true'\n ) {\n const streamManager = opts?.streamManager;\n if (streamManager && streamId) {\n await subscribeToStream(streamManager, streamId, response);\n return;\n }\n const streamIdToUse = randomUUID();\n const headers = {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n };\n if (streamManager) {\n headers['x-genkit-stream-id'] = streamIdToUse;\n }\n response.writeHead(200, headers);\n runActionWithDurableStreaming(\n action,\n streamManager,\n streamIdToUse,\n input,\n context,\n response,\n abortController.signal\n );\n } else {\n try {\n const result = await action.run(input, {\n context,\n abortSignal: abortController.signal,\n });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\nasync function runActionWithDurableStreaming<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n streamManager: StreamManager | undefined,\n streamId: string,\n input: z.infer<I>,\n context: ActionContext,\n response: express.Response,\n abortSignal: AbortSignal\n) {\n let taskQueue: AsyncTaskQueue | undefined;\n let durableStream: ActionStreamInput<any, any> | undefined;\n if (streamManager) {\n taskQueue = new AsyncTaskQueue();\n durableStream = await streamManager.open(streamId);\n }\n try {\n let onChunk = (chunk: z.infer<S>) => {\n // The client may have disconnected mid-stream; writing to a destroyed\n // response would throw.\n if (response.destroyed) return;\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n if (streamManager) {\n const originalOnChunk = onChunk;\n onChunk = (chunk: z.infer<S>) => {\n originalOnChunk(chunk);\n taskQueue!.enqueue(() => durableStream!.write(chunk));\n };\n }\n const result = await action.run(input, {\n onChunk,\n context,\n abortSignal,\n });\n if (streamManager) {\n taskQueue!.enqueue(() => durableStream!.done(result.result));\n await taskQueue!.merge();\n }\n if (!response.destroyed) {\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n }\n } catch (e) {\n if (durableStream) {\n taskQueue!.enqueue(() => durableStream!.error(e));\n await taskQueue!.merge();\n }\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${\n (e as Error).stack\n }`\n );\n if (!response.destroyed) {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n }\n }\n}\n\nasync function subscribeToStream(\n streamManager: StreamManager,\n streamId: string,\n response: express.Response\n): Promise<void> {\n try {\n await streamManager.subscribe(streamId, {\n onChunk: (chunk) => {\n // The subscribing client may have disconnected; skip writes to a\n // destroyed response to avoid throwing.\n if (response.destroyed) return;\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n },\n onDone: (output) => {\n if (response.destroyed) return;\n response.write(\n 'data: ' + JSON.stringify({ result: output }) + streamDelimiter\n );\n response.end();\n },\n onError: (err) => {\n logger.error(\n `Streaming request failed with error: ${(err as Error).message}\\n${\n (err as Error).stack\n }`\n );\n if (response.destroyed) return;\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(err),\n })}${streamDelimiter}`\n );\n response.end();\n },\n });\n } catch (e: any) {\n // The subscribing client may have disconnected; skip writes to a\n // destroyed response to avoid throwing.\n if (response.destroyed) return;\n if (e instanceof StreamNotFoundError) {\n response.status(204).end();\n return;\n }\n if (e.status === 'DEADLINE_EXCEEDED') {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n return;\n }\n throw e;\n }\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * A wrapper object containing a flow with its associated options.\n */\nexport type FlowWithOptions<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n };\n};\n\n/**\n * Adds an auth policy to the flow.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withFlowOptions<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n }\n): FlowWithOptions<I, O, S> {\n return {\n flow,\n options,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (\n | Flow<any, any, any>\n | FlowWithContextProvider<any, any, any>\n | FlowWithOptions<any, any, any>\n )[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('flow' in flow) {\n const flowPath = `/${pathPrefix}${\n ('options' in flow && flow.options.path) || flow.flow.__action.name\n }`;\n logger.debug(` - ${flowPath}`);\n const options =\n 'options' in flow ? flow.options : { contextProvider: flow.context };\n server.post(flowPath, expressHandler(flow.flow, options));\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? Number.parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":["express","bodyParser","cors"]}
package/lib/index.mjs CHANGED
@@ -29,9 +29,10 @@ function expressHandler(action, opts) {
29
29
  context = await opts?.contextProvider?.({
30
30
  method: request.method,
31
31
  headers: Object.fromEntries(
32
- Object.entries(request.headers).map(([key, value]) => [
32
+ Object.entries(request.headers).filter(([, value]) => value !== void 0).map(([key, value]) => [
33
33
  key.toLowerCase(),
34
- Array.isArray(value) ? value.join(" ") : String(value)
34
+ // RFC 9110 5.3: combine repeated field lines with a comma.
35
+ Array.isArray(value) ? value.join(", ") : String(value)
35
36
  ])
36
37
  ),
37
38
  input
@@ -51,7 +52,7 @@ ${e.stack}`
51
52
  request.on("timeout", () => {
52
53
  abortController.abort();
53
54
  });
54
- if (request.get("Accept") === "text/event-stream" || stream === "true") {
55
+ if (request.get("Accept")?.toLowerCase().includes("text/event-stream") || stream === "true") {
55
56
  const streamManager = opts?.streamManager;
56
57
  if (streamManager && streamId) {
57
58
  await subscribeToStream(streamManager, streamId, response);
@@ -105,6 +106,7 @@ async function runActionWithDurableStreaming(action, streamManager, streamId, in
105
106
  }
106
107
  try {
107
108
  let onChunk = (chunk) => {
109
+ if (response.destroyed) return;
108
110
  response.write(
109
111
  "data: " + JSON.stringify({ message: chunk }) + streamDelimiter
110
112
  );
@@ -125,10 +127,12 @@ async function runActionWithDurableStreaming(action, streamManager, streamId, in
125
127
  taskQueue.enqueue(() => durableStream.done(result.result));
126
128
  await taskQueue.merge();
127
129
  }
128
- response.write(
129
- "data: " + JSON.stringify({ result: result.result }) + streamDelimiter
130
- );
131
- response.end();
130
+ if (!response.destroyed) {
131
+ response.write(
132
+ "data: " + JSON.stringify({ result: result.result }) + streamDelimiter
133
+ );
134
+ response.end();
135
+ }
132
136
  } catch (e) {
133
137
  if (durableStream) {
134
138
  taskQueue.enqueue(() => durableStream.error(e));
@@ -138,23 +142,27 @@ async function runActionWithDurableStreaming(action, streamManager, streamId, in
138
142
  `Streaming request failed with error: ${e.message}
139
143
  ${e.stack}`
140
144
  );
141
- response.write(
142
- `error: ${JSON.stringify({
143
- error: getCallableJSON(e)
144
- })}${streamDelimiter}`
145
- );
146
- response.end();
145
+ if (!response.destroyed) {
146
+ response.write(
147
+ `error: ${JSON.stringify({
148
+ error: getCallableJSON(e)
149
+ })}${streamDelimiter}`
150
+ );
151
+ response.end();
152
+ }
147
153
  }
148
154
  }
149
155
  async function subscribeToStream(streamManager, streamId, response) {
150
156
  try {
151
157
  await streamManager.subscribe(streamId, {
152
158
  onChunk: (chunk) => {
159
+ if (response.destroyed) return;
153
160
  response.write(
154
161
  "data: " + JSON.stringify({ message: chunk }) + streamDelimiter
155
162
  );
156
163
  },
157
164
  onDone: (output) => {
165
+ if (response.destroyed) return;
158
166
  response.write(
159
167
  "data: " + JSON.stringify({ result: output }) + streamDelimiter
160
168
  );
@@ -165,6 +173,7 @@ async function subscribeToStream(streamManager, streamId, response) {
165
173
  `Streaming request failed with error: ${err.message}
166
174
  ${err.stack}`
167
175
  );
176
+ if (response.destroyed) return;
168
177
  response.write(
169
178
  `error: ${JSON.stringify({
170
179
  error: getCallableJSON(err)
@@ -174,6 +183,7 @@ ${err.stack}`
174
183
  }
175
184
  });
176
185
  } catch (e) {
186
+ if (response.destroyed) return;
177
187
  if (e instanceof StreamNotFoundError) {
178
188
  response.status(204).end();
179
189
  return;
package/lib/index.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { type CorsOptions } from 'cors';\nimport { randomUUID } from 'crypto';\nimport express from 'express';\nimport {\n Action,\n ActionStreamInput,\n AsyncTaskQueue,\n Flow,\n StreamNotFoundError,\n type ActionContext,\n type StreamManager,\n type z,\n} from 'genkit/beta';\nimport {\n getCallableJSON,\n getHttpStatus,\n type ContextProvider,\n type RequestData,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport type { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n streamManager?: StreamManager;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n const streamIdHeader = request.headers['x-genkit-stream-id'];\n const streamId = Array.isArray(streamIdHeader)\n ? streamIdHeader[0]\n : streamIdHeader;\n\n if (!request.body) {\n const errMsg =\n `Error: request.body is undefined. ` +\n `Possible reasons: missing 'content-type: application/json' in request ` +\n `headers or misconfigured JSON middleware ('app.use(express.json()')? `;\n logger.error(errMsg);\n response\n .status(400)\n .json({ message: errMsg, status: 'INVALID ARGUMENT' })\n .end();\n return;\n }\n\n const input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [\n key.toLowerCase(),\n Array.isArray(value) ? value.join(' ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n const abortController = new AbortController();\n request.on('close', () => {\n abortController.abort();\n });\n // when/if using timeout middleware, it will emit 'timeout' event.\n request.on('timeout', () => {\n abortController.abort();\n });\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n const streamManager = opts?.streamManager;\n if (streamManager && streamId) {\n await subscribeToStream(streamManager, streamId, response);\n return;\n }\n const streamIdToUse = randomUUID();\n const headers = {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n };\n if (streamManager) {\n headers['x-genkit-stream-id'] = streamIdToUse;\n }\n response.writeHead(200, headers);\n runActionWithDurableStreaming(\n action,\n streamManager,\n streamIdToUse,\n input,\n context,\n response,\n abortController.signal\n );\n } else {\n try {\n const result = await action.run(input, {\n context,\n abortSignal: abortController.signal,\n });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\nasync function runActionWithDurableStreaming<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n streamManager: StreamManager | undefined,\n streamId: string,\n input: z.infer<I>,\n context: ActionContext,\n response: express.Response,\n abortSignal: AbortSignal\n) {\n let taskQueue: AsyncTaskQueue | undefined;\n let durableStream: ActionStreamInput<any, any> | undefined;\n if (streamManager) {\n taskQueue = new AsyncTaskQueue();\n durableStream = await streamManager.open(streamId);\n }\n try {\n let onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n if (streamManager) {\n const originalOnChunk = onChunk;\n onChunk = (chunk: z.infer<S>) => {\n originalOnChunk(chunk);\n taskQueue!.enqueue(() => durableStream!.write(chunk));\n };\n }\n const result = await action.run(input, {\n onChunk,\n context,\n abortSignal,\n });\n if (streamManager) {\n taskQueue!.enqueue(() => durableStream!.done(result.result));\n await taskQueue!.merge();\n }\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n if (durableStream) {\n taskQueue!.enqueue(() => durableStream!.error(e));\n await taskQueue!.merge();\n }\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${\n (e as Error).stack\n }`\n );\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n }\n}\n\nasync function subscribeToStream(\n streamManager: StreamManager,\n streamId: string,\n response: express.Response\n): Promise<void> {\n try {\n await streamManager.subscribe(streamId, {\n onChunk: (chunk) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n },\n onDone: (output) => {\n response.write(\n 'data: ' + JSON.stringify({ result: output }) + streamDelimiter\n );\n response.end();\n },\n onError: (err) => {\n logger.error(\n `Streaming request failed with error: ${(err as Error).message}\\n${\n (err as Error).stack\n }`\n );\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(err),\n })}${streamDelimiter}`\n );\n response.end();\n },\n });\n } catch (e: any) {\n if (e instanceof StreamNotFoundError) {\n response.status(204).end();\n return;\n }\n if (e.status === 'DEADLINE_EXCEEDED') {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n return;\n }\n throw e;\n }\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * A wrapper object containing a flow with its associated options.\n */\nexport type FlowWithOptions<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n };\n};\n\n/**\n * Adds an auth policy to the flow.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withFlowOptions<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n }\n): FlowWithOptions<I, O, S> {\n return {\n flow,\n options,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (\n | Flow<any, any, any>\n | FlowWithContextProvider<any, any, any>\n | FlowWithOptions<any, any, any>\n )[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('flow' in flow) {\n const flowPath = `/${pathPrefix}${\n ('options' in flow && flow.options.path) || flow.flow.__action.name\n }`;\n logger.debug(` - ${flowPath}`);\n const options =\n 'options' in flow ? flow.options : { contextProvider: flow.context };\n server.post(flowPath, expressHandler(flow.flow, options));\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? Number.parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { type CorsOptions } from 'cors';\nimport { randomUUID } from 'crypto';\nimport express from 'express';\nimport {\n Action,\n ActionStreamInput,\n AsyncTaskQueue,\n Flow,\n StreamNotFoundError,\n type ActionContext,\n type StreamManager,\n type z,\n} from 'genkit/beta';\nimport {\n getCallableJSON,\n getHttpStatus,\n type ContextProvider,\n type RequestData,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport type { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n streamManager?: StreamManager;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n const streamIdHeader = request.headers['x-genkit-stream-id'];\n const streamId = Array.isArray(streamIdHeader)\n ? streamIdHeader[0]\n : streamIdHeader;\n\n if (!request.body) {\n const errMsg =\n `Error: request.body is undefined. ` +\n `Possible reasons: missing 'content-type: application/json' in request ` +\n `headers or misconfigured JSON middleware ('app.use(express.json()')? `;\n logger.error(errMsg);\n response\n .status(400)\n .json({ message: errMsg, status: 'INVALID ARGUMENT' })\n .end();\n return;\n }\n\n const input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers)\n // Skip headers explicitly set to undefined so they don't become\n // the literal string \"undefined\" via String(value).\n .filter(([, value]) => value !== undefined)\n .map(([key, value]) => [\n key.toLowerCase(),\n // RFC 9110 5.3: combine repeated field lines with a comma.\n Array.isArray(value) ? value.join(', ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n const abortController = new AbortController();\n request.on('close', () => {\n abortController.abort();\n });\n // when/if using timeout middleware, it will emit 'timeout' event.\n request.on('timeout', () => {\n abortController.abort();\n });\n\n if (\n request.get('Accept')?.toLowerCase().includes('text/event-stream') ||\n stream === 'true'\n ) {\n const streamManager = opts?.streamManager;\n if (streamManager && streamId) {\n await subscribeToStream(streamManager, streamId, response);\n return;\n }\n const streamIdToUse = randomUUID();\n const headers = {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n };\n if (streamManager) {\n headers['x-genkit-stream-id'] = streamIdToUse;\n }\n response.writeHead(200, headers);\n runActionWithDurableStreaming(\n action,\n streamManager,\n streamIdToUse,\n input,\n context,\n response,\n abortController.signal\n );\n } else {\n try {\n const result = await action.run(input, {\n context,\n abortSignal: abortController.signal,\n });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\nasync function runActionWithDurableStreaming<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n streamManager: StreamManager | undefined,\n streamId: string,\n input: z.infer<I>,\n context: ActionContext,\n response: express.Response,\n abortSignal: AbortSignal\n) {\n let taskQueue: AsyncTaskQueue | undefined;\n let durableStream: ActionStreamInput<any, any> | undefined;\n if (streamManager) {\n taskQueue = new AsyncTaskQueue();\n durableStream = await streamManager.open(streamId);\n }\n try {\n let onChunk = (chunk: z.infer<S>) => {\n // The client may have disconnected mid-stream; writing to a destroyed\n // response would throw.\n if (response.destroyed) return;\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n if (streamManager) {\n const originalOnChunk = onChunk;\n onChunk = (chunk: z.infer<S>) => {\n originalOnChunk(chunk);\n taskQueue!.enqueue(() => durableStream!.write(chunk));\n };\n }\n const result = await action.run(input, {\n onChunk,\n context,\n abortSignal,\n });\n if (streamManager) {\n taskQueue!.enqueue(() => durableStream!.done(result.result));\n await taskQueue!.merge();\n }\n if (!response.destroyed) {\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n }\n } catch (e) {\n if (durableStream) {\n taskQueue!.enqueue(() => durableStream!.error(e));\n await taskQueue!.merge();\n }\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${\n (e as Error).stack\n }`\n );\n if (!response.destroyed) {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n }\n }\n}\n\nasync function subscribeToStream(\n streamManager: StreamManager,\n streamId: string,\n response: express.Response\n): Promise<void> {\n try {\n await streamManager.subscribe(streamId, {\n onChunk: (chunk) => {\n // The subscribing client may have disconnected; skip writes to a\n // destroyed response to avoid throwing.\n if (response.destroyed) return;\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n },\n onDone: (output) => {\n if (response.destroyed) return;\n response.write(\n 'data: ' + JSON.stringify({ result: output }) + streamDelimiter\n );\n response.end();\n },\n onError: (err) => {\n logger.error(\n `Streaming request failed with error: ${(err as Error).message}\\n${\n (err as Error).stack\n }`\n );\n if (response.destroyed) return;\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(err),\n })}${streamDelimiter}`\n );\n response.end();\n },\n });\n } catch (e: any) {\n // The subscribing client may have disconnected; skip writes to a\n // destroyed response to avoid throwing.\n if (response.destroyed) return;\n if (e instanceof StreamNotFoundError) {\n response.status(204).end();\n return;\n }\n if (e.status === 'DEADLINE_EXCEEDED') {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n return;\n }\n throw e;\n }\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * A wrapper object containing a flow with its associated options.\n */\nexport type FlowWithOptions<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n };\n};\n\n/**\n * Adds an auth policy to the flow.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withFlowOptions<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n }\n): FlowWithOptions<I, O, S> {\n return {\n flow,\n options,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (\n | Flow<any, any, any>\n | FlowWithContextProvider<any, any, any>\n | FlowWithOptions<any, any, any>\n )[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('flow' in flow) {\n const flowPath = `/${pathPrefix}${\n ('options' in flow && flow.options.path) || flow.flow.__action.name\n }`;\n logger.debug(` - ${flowPath}`);\n const options =\n 'options' in flow ? flow.options : { contextProvider: flow.context };\n server.post(flowPath, expressHandler(flow.flow, options));\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? Number.parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "genai",
10
10
  "generative-ai"
11
11
  ],
12
- "version": "1.35.0",
12
+ "version": "1.37.0",
13
13
  "type": "commonjs",
14
14
  "repository": {
15
15
  "type": "git",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "peerDependencies": {
26
26
  "express": "^4.21.1 || ^5",
27
- "genkit": "^1.35.0"
27
+ "genkit": "^1.37.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "get-port": "^5.1.0",
package/src/index.ts CHANGED
@@ -85,10 +85,15 @@ export function expressHandler<
85
85
  (await opts?.contextProvider?.({
86
86
  method: request.method as RequestData['method'],
87
87
  headers: Object.fromEntries(
88
- Object.entries(request.headers).map(([key, value]) => [
89
- key.toLowerCase(),
90
- Array.isArray(value) ? value.join(' ') : String(value),
91
- ])
88
+ Object.entries(request.headers)
89
+ // Skip headers explicitly set to undefined so they don't become
90
+ // the literal string "undefined" via String(value).
91
+ .filter(([, value]) => value !== undefined)
92
+ .map(([key, value]) => [
93
+ key.toLowerCase(),
94
+ // RFC 9110 5.3: combine repeated field lines with a comma.
95
+ Array.isArray(value) ? value.join(', ') : String(value),
96
+ ])
92
97
  ),
93
98
  input,
94
99
  })) || {};
@@ -109,7 +114,10 @@ export function expressHandler<
109
114
  abortController.abort();
110
115
  });
111
116
 
112
- if (request.get('Accept') === 'text/event-stream' || stream === 'true') {
117
+ if (
118
+ request.get('Accept')?.toLowerCase().includes('text/event-stream') ||
119
+ stream === 'true'
120
+ ) {
113
121
  const streamManager = opts?.streamManager;
114
122
  if (streamManager && streamId) {
115
123
  await subscribeToStream(streamManager, streamId, response);
@@ -180,6 +188,9 @@ async function runActionWithDurableStreaming<
180
188
  }
181
189
  try {
182
190
  let onChunk = (chunk: z.infer<S>) => {
191
+ // The client may have disconnected mid-stream; writing to a destroyed
192
+ // response would throw.
193
+ if (response.destroyed) return;
183
194
  response.write(
184
195
  'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter
185
196
  );
@@ -200,10 +211,12 @@ async function runActionWithDurableStreaming<
200
211
  taskQueue!.enqueue(() => durableStream!.done(result.result));
201
212
  await taskQueue!.merge();
202
213
  }
203
- response.write(
204
- 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter
205
- );
206
- response.end();
214
+ if (!response.destroyed) {
215
+ response.write(
216
+ 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter
217
+ );
218
+ response.end();
219
+ }
207
220
  } catch (e) {
208
221
  if (durableStream) {
209
222
  taskQueue!.enqueue(() => durableStream!.error(e));
@@ -214,12 +227,14 @@ async function runActionWithDurableStreaming<
214
227
  (e as Error).stack
215
228
  }`
216
229
  );
217
- response.write(
218
- `error: ${JSON.stringify({
219
- error: getCallableJSON(e),
220
- })}${streamDelimiter}`
221
- );
222
- response.end();
230
+ if (!response.destroyed) {
231
+ response.write(
232
+ `error: ${JSON.stringify({
233
+ error: getCallableJSON(e),
234
+ })}${streamDelimiter}`
235
+ );
236
+ response.end();
237
+ }
223
238
  }
224
239
  }
225
240
 
@@ -231,11 +246,15 @@ async function subscribeToStream(
231
246
  try {
232
247
  await streamManager.subscribe(streamId, {
233
248
  onChunk: (chunk) => {
249
+ // The subscribing client may have disconnected; skip writes to a
250
+ // destroyed response to avoid throwing.
251
+ if (response.destroyed) return;
234
252
  response.write(
235
253
  'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter
236
254
  );
237
255
  },
238
256
  onDone: (output) => {
257
+ if (response.destroyed) return;
239
258
  response.write(
240
259
  'data: ' + JSON.stringify({ result: output }) + streamDelimiter
241
260
  );
@@ -247,6 +266,7 @@ async function subscribeToStream(
247
266
  (err as Error).stack
248
267
  }`
249
268
  );
269
+ if (response.destroyed) return;
250
270
  response.write(
251
271
  `error: ${JSON.stringify({
252
272
  error: getCallableJSON(err),
@@ -256,6 +276,9 @@ async function subscribeToStream(
256
276
  },
257
277
  });
258
278
  } catch (e: any) {
279
+ // The subscribing client may have disconnected; skip writes to a
280
+ // destroyed response to avoid throwing.
281
+ if (response.destroyed) return;
259
282
  if (e instanceof StreamNotFoundError) {
260
283
  response.status(204).end();
261
284
  return;
@@ -405,7 +405,7 @@ describe('expressHandler', async () => {
405
405
  assert.strictEqual(await subscription.output, 'Echo: durable');
406
406
  });
407
407
 
408
- it.only('should return 204 for a non-existent stream', async () => {
408
+ it('should return 204 for a non-existent stream', async () => {
409
409
  try {
410
410
  const result = streamFlow({
411
411
  url: `http://localhost:${port}/streamingFlowDurable`,
@@ -422,6 +422,22 @@ describe('expressHandler', async () => {
422
422
  }
423
423
  });
424
424
 
425
+ it('detects streaming for a multi-value, mixed-case Accept header', async () => {
426
+ // Clients/proxies can send a media-type list and mixed casing, e.g.
427
+ // "Text/Event-Stream, */*"; the handler should still stream rather than
428
+ // fall back to a single JSON response.
429
+ const response = await fetch(`http://localhost:${port}/streamingFlow`, {
430
+ method: 'POST',
431
+ headers: {
432
+ 'Content-Type': 'application/json',
433
+ Accept: 'Text/Event-Stream, */*',
434
+ },
435
+ body: JSON.stringify({ data: { question: 'hi' } }),
436
+ });
437
+ const text = await response.text();
438
+ assert.match(text, /^data: /m); // SSE frames, not a single JSON body
439
+ });
440
+
425
441
  it('stream a model', async () => {
426
442
  const result = streamFlow({
427
443
  url: `http://localhost:${port}/echoModel`,