spiceflow 1.1.17 → 1.2.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.
Files changed (47) hide show
  1. package/README.md +236 -3
  2. package/dist/client/types.d.ts +1 -1
  3. package/dist/client/types.d.ts.map +1 -1
  4. package/dist/client.test.js +36 -1
  5. package/dist/client.test.js.map +1 -1
  6. package/dist/index.d.ts +2 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp-transport.d.ts +45 -0
  10. package/dist/mcp-transport.d.ts.map +1 -0
  11. package/dist/mcp-transport.js +107 -0
  12. package/dist/mcp-transport.js.map +1 -0
  13. package/dist/mcp.d.ts +36 -0
  14. package/dist/mcp.d.ts.map +1 -0
  15. package/dist/mcp.js +211 -0
  16. package/dist/mcp.js.map +1 -0
  17. package/dist/mcp.test.d.ts +2 -0
  18. package/dist/mcp.test.d.ts.map +1 -0
  19. package/dist/mcp.test.js +224 -0
  20. package/dist/mcp.test.js.map +1 -0
  21. package/dist/openapi.d.ts +14 -27
  22. package/dist/openapi.d.ts.map +1 -1
  23. package/dist/openapi.js +101 -49
  24. package/dist/openapi.js.map +1 -1
  25. package/dist/openapi.test.js +242 -18
  26. package/dist/openapi.test.js.map +1 -1
  27. package/dist/spiceflow.d.ts +5 -3
  28. package/dist/spiceflow.d.ts.map +1 -1
  29. package/dist/spiceflow.js +42 -10
  30. package/dist/spiceflow.js.map +1 -1
  31. package/dist/spiceflow.test.js +21 -3
  32. package/dist/spiceflow.test.js.map +1 -1
  33. package/dist/types.d.ts +7 -13
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js.map +1 -1
  36. package/package.json +15 -2
  37. package/src/client/types.ts +3 -5
  38. package/src/client.test.ts +45 -2
  39. package/src/index.ts +2 -1
  40. package/src/mcp-transport.ts +148 -0
  41. package/src/mcp.test.ts +273 -0
  42. package/src/mcp.ts +270 -0
  43. package/src/openapi.test.ts +238 -18
  44. package/src/openapi.ts +136 -66
  45. package/src/spiceflow.test.ts +27 -3
  46. package/src/spiceflow.ts +83 -13
  47. package/src/types.ts +129 -140
package/src/openapi.ts CHANGED
@@ -12,7 +12,18 @@ import deepClone from 'lodash.clonedeep'
12
12
  import { z } from 'zod'
13
13
  import zodToJsonSchema from 'zod-to-json-schema'
14
14
 
15
- export const toOpenAPIPath = (path: string) =>
15
+ const extractParamNames = (path: string): string[] => {
16
+ return path.split('/').reduce((params: string[], segment) => {
17
+ if (segment.startsWith(':')) {
18
+ let param = segment.slice(1)
19
+ if (param.endsWith('?')) param = param.slice(0, -1)
20
+ params.push(param)
21
+ }
22
+ return params
23
+ }, [])
24
+ }
25
+
26
+ const toOpenAPIPath = (path: string) =>
16
27
  path
17
28
  .split('/')
18
29
  .map((x) => {
@@ -26,7 +37,7 @@ export const toOpenAPIPath = (path: string) =>
26
37
  })
27
38
  .join('/')
28
39
 
29
- export const mapProperties = (
40
+ const mapProperties = (
30
41
  name: string,
31
42
  schema: TypeSchema | string | undefined,
32
43
  models: Record<string, TypeSchema>,
@@ -61,7 +72,7 @@ export const mapProperties = (
61
72
 
62
73
  const mapTypesResponse = (
63
74
  types: string[],
64
- schema:
75
+ schema?:
65
76
  | string
66
77
  | {
67
78
  type: string
@@ -90,10 +101,10 @@ const mapTypesResponse = (
90
101
  return responses
91
102
  }
92
103
 
93
- export const capitalize = (word: string) =>
104
+ const capitalize = (word: string) =>
94
105
  word.charAt(0).toUpperCase() + word.slice(1)
95
106
 
96
- export const generateOperationId = (method: string, paths: string) => {
107
+ const generateOperationId = (method: string, paths: string) => {
97
108
  let operationId = method.toLowerCase()
98
109
 
99
110
  if (paths === '/') return operationId + 'Index'
@@ -109,42 +120,73 @@ export const generateOperationId = (method: string, paths: string) => {
109
120
  return operationId
110
121
  }
111
122
 
112
- export const registerSchemaPath = ({
123
+ const registerSchemaPath = ({
113
124
  schema,
114
- path,
115
- method,
116
- hook,
125
+ route,
117
126
  models,
118
127
  }: {
119
128
  schema: Partial<OpenAPIV3.PathsObject>
120
- contentType?: string | string[]
121
- path: string
122
- method: HTTPMethod
123
- hook?: LocalHook<any, any, any, any, any, any, any>
129
+ route: InternalRoute
124
130
  models: Record<string, TypeSchema>
125
131
  }) => {
126
- if (hook) hook = deepClone(hook)
132
+ const hook = route.hooks ? deepClone(route.hooks) : undefined
127
133
 
128
- // TODO if a route uses an async generator, add text/event-stream. if a roue does not add an explicit schema, use all possible content types
129
- const contentType = hook?.type ?? [
130
- 'application/json',
131
- // 'multipart/form-data',
132
- // 'text/plain',
133
- ]
134
+ let contentTypes = ['application/json']
134
135
 
135
- path = toOpenAPIPath(path)
136
+ if (isAsyncGenerator(route.handler) && !route.hooks?.response) {
137
+ contentTypes = ['text/event-stream']
138
+ } else if (hook?.type) {
139
+ contentTypes = Array.isArray(hook.type) ? hook.type : [hook.type]
140
+ }
136
141
 
137
- const contentTypes =
138
- typeof contentType === 'string'
139
- ? [contentType]
140
- : contentType ?? ['application/json']
142
+ const path = toOpenAPIPath(route.path)
141
143
 
142
144
  const bodySchema = getJsonSchema(hook?.body)
143
- const paramsSchema = hook?.params
145
+ let paramsSchema = hook?.params
146
+ if (route.path.includes(':') && !paramsSchema) {
147
+ const paramNames = extractParamNames(route.path)
148
+ if (paramNames.length) {
149
+ // Create a schema object with string parameters for each URL param
150
+ const paramSchemaObject = {}
151
+ for (const param of paramNames) {
152
+ paramSchemaObject[param] = z.string()
153
+ }
154
+ paramsSchema = z.object(paramSchemaObject)
155
+ }
156
+ }
157
+
144
158
  // const headerSchema = hook?.headers
145
159
  const querySchema = hook?.query
146
160
  let responseSchema = hook?.response as unknown as TypeSchema
147
- let openapiResponse: OpenAPIV3.ResponsesObject = {}
161
+ const defaultResponseSchema: OpenAPIV3.ResponsesObject = {
162
+ // '500': {
163
+ // description: 'Internal Server Error',
164
+ // content: {
165
+ // 'text/plain': {
166
+ // schema: {
167
+ // type: 'string',
168
+ // },
169
+ // },
170
+ // },
171
+ // },
172
+ '200': {
173
+ description: '',
174
+ content: {
175
+ '*/*': {
176
+ schema: {},
177
+ },
178
+ },
179
+ },
180
+ default: {
181
+ description: '',
182
+ content: {
183
+ '*/*': {
184
+ schema: {},
185
+ },
186
+ },
187
+ },
188
+ }
189
+ let openapiResponse: OpenAPIV3.ResponsesObject = defaultResponseSchema
148
190
 
149
191
  if (typeof responseSchema === 'object') {
150
192
  const isStatusMap = Object.keys(responseSchema).every(
@@ -162,6 +204,7 @@ export const registerSchemaPath = ({
162
204
  } = jsonSchema
163
205
 
164
206
  openapiResponse = {
207
+ ...defaultResponseSchema,
165
208
  '200': {
166
209
  ...rest,
167
210
  description: (rest.description as any) || '',
@@ -213,7 +256,7 @@ export const registerSchemaPath = ({
213
256
 
214
257
  openapiResponse[key] = {
215
258
  ...rest,
216
- description: rest.description as any || '',
259
+ description: (rest.description as any) || '',
217
260
  content: mapTypesResponse(
218
261
  contentTypes,
219
262
  type === 'object' || type === 'array'
@@ -245,6 +288,7 @@ export const registerSchemaPath = ({
245
288
  } = getJsonSchema(models[responseSchema])
246
289
 
247
290
  openapiResponse = {
291
+ ...defaultResponseSchema,
248
292
  // @ts-ignore
249
293
  '200': {
250
294
  description: '',
@@ -260,27 +304,42 @@ export const registerSchemaPath = ({
260
304
  ...mapProperties('path', paramsSchema, models),
261
305
  ...mapProperties('query', querySchema, models),
262
306
  ]
263
-
264
307
  schema[path] = {
265
- ...(schema[path] ? schema[path] : {}),
266
- [method.toLowerCase()]: {
267
- ...((paramsSchema || querySchema || bodySchema
268
- ? ({ parameters } as any)
269
- : {}) satisfies OpenAPIV3.ParameterObject),
270
- ...(!isObjEmpty(openapiResponse)
308
+ // Merge with existing path schema if it exists
309
+ ...(schema[path] ?? {}),
310
+ [route.method.toLowerCase()]: {
311
+ // Add streaming flag for async generators
312
+ ...(isAsyncGenerator(route.handler) && {
313
+ 'x-fern-streaming': {
314
+ format: 'sse',
315
+ },
316
+ }),
317
+
318
+ // Add parameters if any schemas are defined
319
+ ...(paramsSchema || querySchema || bodySchema
271
320
  ? {
272
- responses: openapiResponse,
321
+ parameters,
273
322
  }
274
323
  : {}),
275
- operationId:
276
- hook?.detail?.operationId ?? generateOperationId(method, path),
324
+
325
+ // Add responses if defined
326
+ ...(!isObjEmpty(openapiResponse) && {
327
+ responses: openapiResponse,
328
+ }),
329
+
330
+ // operationId:
331
+ // hook?.detail?.operationId ?? generateOperationId(route.method, path),
332
+
333
+ // Add any additional details from hook
277
334
  ...hook?.detail,
335
+
336
+ // Add request body if body schema exists
278
337
  ...(bodySchema
279
338
  ? {
280
339
  requestBody: {
281
340
  required: true,
282
341
  content: mapTypesResponse(
283
- contentTypes,
342
+ hook.bodyType ? [hook.bodyType] : ['application/json'],
284
343
  typeof bodySchema === 'string'
285
344
  ? {
286
345
  $ref: `#/components/schemas/${bodySchema}`,
@@ -301,20 +360,27 @@ export const registerSchemaPath = ({
301
360
  */
302
361
  export const openapi = <Path extends string = '/openapi'>({
303
362
  path = '/openapi' as Path,
304
- documentation = {},
363
+ ...additional
305
364
  }: {
306
365
  path?: Path
307
- /**
308
- * Customize Swagger config, refers to Swagger 2.0 config
309
- *
310
- * @see https://swagger.io/specification/v2/
311
- */
312
- documentation?: Omit<
313
- Partial<OpenAPIV3.Document>,
314
- | 'x-express-openapi-additional-middleware'
315
- | 'x-express-openapi-validation-strict'
316
- >
317
- } = {}) => {
366
+ } & Omit<
367
+ Partial<OpenAPIV3.Document>,
368
+ | 'x-express-openapi-additional-middleware'
369
+ | 'x-express-openapi-validation-strict'
370
+ > & {
371
+ 'x-fern-global-headers'?: Array<{
372
+ header: string
373
+ name: string
374
+ optional?: boolean
375
+ }>
376
+ 'x-fern-version'?: {
377
+ version: {
378
+ header: string
379
+ default: string
380
+ values: string[]
381
+ }
382
+ }
383
+ } = {}) => {
318
384
  const schema = {}
319
385
  let totalRoutes = 0
320
386
 
@@ -349,12 +415,9 @@ export const openapi = <Path extends string = '/openapi'>({
349
415
  ALLOWED_METHODS.forEach((method) => {
350
416
  registerSchemaPath({
351
417
  schema,
352
- hook: route.hooks,
353
- method,
354
- path: route.path,
418
+ route: { ...route, method },
355
419
  // @ts-ignore
356
420
  models: app.definitions?.type,
357
- contentType: route.hooks?.type,
358
421
  })
359
422
  })
360
423
  return
@@ -362,12 +425,9 @@ export const openapi = <Path extends string = '/openapi'>({
362
425
 
363
426
  registerSchemaPath({
364
427
  schema,
365
- hook: route.hooks,
366
- method: route.method,
367
- path: route.path,
428
+ route,
368
429
  // @ts-ignore
369
430
  models: app.definitions?.type,
370
- contentType: route.hooks?.type,
371
431
  })
372
432
  })
373
433
  }
@@ -375,7 +435,7 @@ export const openapi = <Path extends string = '/openapi'>({
375
435
  return {
376
436
  openapi: '3.1.3',
377
437
  ...{
378
- ...documentation,
438
+ ...additional,
379
439
  // tags: documentation.tags?.filter(
380
440
  // (tag) => !excludeTags?.includes(tag?.name),
381
441
  // ),
@@ -383,19 +443,19 @@ export const openapi = <Path extends string = '/openapi'>({
383
443
  title: 'Spiceflow Documentation',
384
444
  description: 'Development documentation',
385
445
  version: '0.0.0',
386
- ...documentation.info,
446
+ ...additional.info,
387
447
  },
388
448
  },
389
449
  paths: {
390
450
  ...schema,
391
- ...documentation.paths,
451
+ ...additional.paths,
392
452
  },
393
453
  components: {
394
- ...documentation.components,
454
+ ...additional.components,
395
455
  schemas: {
396
456
  // @ts-ignore
397
457
  ...app.definitions?.type,
398
- ...documentation.components?.schemas,
458
+ ...additional.components?.schemas,
399
459
  },
400
460
  },
401
461
  } satisfies OpenAPIV3.Document
@@ -409,12 +469,22 @@ function getJsonSchema(schema: TypeSchema): JSONSchemaType<any> {
409
469
  if (isZodSchema(schema)) {
410
470
  let fn = zodToJsonSchema.default ?? zodToJsonSchema
411
471
  let jsonSchema = fn(schema, {})
412
- return jsonSchema as any
472
+ const { $schema, ...rest } = jsonSchema
473
+ return rest as any
413
474
  }
414
475
 
415
- return schema as any
476
+ const { $schema, ...rest } = schema as any
477
+ return rest as any
416
478
  }
417
479
 
418
480
  function isObjEmpty(obj: Record<string, any>) {
419
481
  return obj === undefined || Object.keys(obj).length === 0
420
482
  }
483
+
484
+ function isAsyncGenerator(fn: any): boolean {
485
+ return (
486
+ fn &&
487
+ typeof fn === 'function' &&
488
+ fn.constructor?.name === 'AsyncGeneratorFunction'
489
+ )
490
+ }
@@ -18,6 +18,30 @@ test('dynamic route', async () => {
18
18
  expect(res.status).toBe(200)
19
19
  expect(await res.json()).toEqual('hi')
20
20
  })
21
+ test('handler returns url encoded data', async () => {
22
+ const params = new URLSearchParams()
23
+ params.append('name', 'test')
24
+ params.append('value', '123')
25
+
26
+ const res = await new Spiceflow()
27
+ .post('/form', () => params, {
28
+ type: 'application/x-www-form-urlencoded',
29
+ })
30
+ .handle(
31
+ new Request('http://localhost/form', {
32
+ method: 'POST',
33
+ }),
34
+ )
35
+
36
+ expect(res.status).toBe(200)
37
+ expect(res.headers.get('content-type')).toBe(
38
+ 'application/x-www-form-urlencoded',
39
+ )
40
+ const text = await res.text()
41
+ const responseParams = new URLSearchParams(text)
42
+ expect(responseParams.get('name')).toBe('test')
43
+ expect(responseParams.get('value')).toBe('123')
44
+ })
21
45
  test('GET dynamic route', async () => {
22
46
  const res = await new Spiceflow()
23
47
  .get('/ids/:id', () => 'hi')
@@ -79,7 +103,7 @@ test('onError fires on validation errors', async () => {
79
103
 
80
104
  expect(res.status).toBe(400)
81
105
  expect(errorMessage).toContain('data/name must be string')
82
- expect(await res.text()).toBe('Error')
106
+ expect(await res.text()).toMatchInlineSnapshot(`"Error"`)
83
107
  })
84
108
 
85
109
  test.todo('HEAD uses GET route, does not add body', async () => {
@@ -287,7 +311,7 @@ test('validate body works, request fails', async () => {
287
311
  )
288
312
  expect(res.status).toBe(422)
289
313
  expect(await res.text()).toMatchInlineSnapshot(
290
- `"data must have required property 'requiredField'"`,
314
+ `"{"message":"data must have required property 'requiredField'"}"`,
291
315
  )
292
316
  })
293
317
 
@@ -554,7 +578,7 @@ test('errors inside basPath works', async () => {
554
578
  expect(onErrorTriggered).toEqual(['root', 'two', 'nested'])
555
579
  expect(onReqTriggered).toEqual(['root', 'two', 'nested'])
556
580
  expect(res.status).toBe(500)
557
- expect(await res.text()).toBe('error message')
581
+ expect(await res.text()).toMatchInlineSnapshot(`"{"message":"error message"}"`)
558
582
  // expect(await res.json()).toEqual('nested'))
559
583
  }
560
584
  })
package/src/spiceflow.ts CHANGED
@@ -7,6 +7,7 @@ export { Type as t }
7
7
  import addFormats from 'ajv-formats'
8
8
  import {
9
9
  ComposeSpiceflowResponse,
10
+ ContentType,
10
11
  CreateClient,
11
12
  DefinitionBase,
12
13
  ErrorHandler,
@@ -66,6 +67,7 @@ type OnError = (x: { error: any; request: Request }) => AsyncResponse
66
67
  export type InternalRoute = {
67
68
  method: HTTPMethod
68
69
  path: string
70
+ type: ContentType
69
71
  handler: InlineHandler<any, any, any>
70
72
  hooks: LocalHook<any, any, any, any, any, any, any>
71
73
  validateBody?: ValidateFunction
@@ -111,7 +113,7 @@ export class Spiceflow<
111
113
  private onErrorHandlers: OnError[] = []
112
114
  private routes: InternalRoute[] = []
113
115
  private defaultState: Record<any, any> = {}
114
- private topLevelApp?: AnySpiceflow
116
+ topLevelApp?: AnySpiceflow
115
117
 
116
118
  /** @internal */
117
119
  prefix?: string
@@ -157,6 +159,7 @@ export class Spiceflow<
157
159
  const store = this.router.register(path)
158
160
  let route: InternalRoute = {
159
161
  ...rest,
162
+ type: hooks?.type || '',
160
163
  method: (method || '') as any,
161
164
  path: path || '',
162
165
  handler: handler!,
@@ -775,9 +778,15 @@ export class Spiceflow<
775
778
  if (isResponse(res)) return res
776
779
 
777
780
  let status = err?.status ?? 500
778
- res ||= new Response(err?.message || 'Internal Server Error', {
779
- status,
780
- })
781
+ res ||= new Response(
782
+ JSON.stringify({ message: err?.message || 'Internal Server Error' }),
783
+ {
784
+ status,
785
+ headers: {
786
+ 'content-type': 'application/json',
787
+ },
788
+ },
789
+ )
781
790
  return res
782
791
  }
783
792
 
@@ -794,7 +803,10 @@ export class Spiceflow<
794
803
  if (!result && index < middlewares.length) {
795
804
  return await next()
796
805
  } else if (result) {
797
- return await turnHandlerResultIntoResponse(result)
806
+ return await turnHandlerResultIntoResponse(
807
+ result,
808
+ route.internalRoute,
809
+ )
798
810
  }
799
811
  }
800
812
  if (handlerResponse) {
@@ -816,10 +828,14 @@ export class Spiceflow<
816
828
  generator: res,
817
829
  request,
818
830
  onErrorHandlers,
831
+ route: route.internalRoute,
819
832
  })
820
833
  return handlerResponse
821
834
  }
822
- handlerResponse = await turnHandlerResultIntoResponse(res)
835
+ handlerResponse = await turnHandlerResultIntoResponse(
836
+ res,
837
+ route.internalRoute,
838
+ )
823
839
  return handlerResponse
824
840
  } catch (err) {
825
841
  handlerResponse = await getResForError(err)
@@ -896,7 +912,7 @@ export class Spiceflow<
896
912
  return appsInScope
897
913
  }
898
914
 
899
- async listen(port: number, hostname: string = '127.0.0.1') {
915
+ async listen(port: number, hostname: string = '0.0.0.0') {
900
916
  // @ts-ignore
901
917
  if (typeof Bun !== 'undefined') {
902
918
  // @ts-ignore
@@ -907,9 +923,12 @@ export class Spiceflow<
907
923
  reusePort: true,
908
924
  error(error) {
909
925
  console.error(error)
910
- return new Response('Internal Server Error', {
911
- status: 500,
912
- })
926
+ return new Response(
927
+ JSON.stringify({ message: 'Internal Server Error' }),
928
+ {
929
+ status: 500,
930
+ },
931
+ )
913
932
  },
914
933
 
915
934
  fetch: async (request) => {
@@ -985,7 +1004,7 @@ export class Spiceflow<
985
1004
  } catch (error) {
986
1005
  console.error('Error handling request:', error)
987
1006
  res.statusCode = 500
988
- res.end('Internal Server Error')
1007
+ res.end(JSON.stringify({ message: 'Internal Server Error' }))
989
1008
  }
990
1009
  })
991
1010
 
@@ -1003,16 +1022,18 @@ export class Spiceflow<
1003
1022
  onErrorHandlers,
1004
1023
  generator,
1005
1024
  request,
1025
+ route,
1006
1026
  }: {
1007
1027
  generator: Generator | AsyncGenerator
1008
1028
  onErrorHandlers: OnError[]
1009
1029
  request: Request
1030
+ route: InternalRoute
1010
1031
  }) {
1011
1032
  let init = generator.next()
1012
1033
  if (init instanceof Promise) init = await init
1013
1034
 
1014
1035
  if (init?.done) {
1015
- return await turnHandlerResultIntoResponse(init.value)
1036
+ return await turnHandlerResultIntoResponse(init.value, route)
1016
1037
  }
1017
1038
  // let errorHandlers = this.routerTree.onErrorHandlers
1018
1039
  let self = this
@@ -1197,7 +1218,10 @@ export function bfs(tree: AnySpiceflow) {
1197
1218
  return nodes
1198
1219
  }
1199
1220
 
1200
- export async function turnHandlerResultIntoResponse(result: any) {
1221
+ export async function turnHandlerResultIntoResponse(
1222
+ result: any,
1223
+ route: InternalRoute,
1224
+ ) {
1201
1225
  // if user returns a promise, await it
1202
1226
  if (result instanceof Promise) {
1203
1227
  result = await result
@@ -1207,6 +1231,52 @@ export async function turnHandlerResultIntoResponse(result: any) {
1207
1231
  return result
1208
1232
  }
1209
1233
 
1234
+ if (route.type) {
1235
+ if (route.type?.includes('multipart/form-data')) {
1236
+ if (!(result instanceof Response)) {
1237
+ throw new Error(
1238
+ `Invalid form data returned from route handler ${
1239
+ route.path
1240
+ } - expected Response but got ${
1241
+ result?.constructor?.name || typeof result
1242
+ }. FormData cannot be returned directly - it must be wrapped in a Response object with the appropriate content-type header.`,
1243
+ )
1244
+ }
1245
+ }
1246
+ if (route.type?.includes('application/x-www-form-urlencoded')) {
1247
+ if (!(result instanceof URLSearchParams)) {
1248
+ throw new Error(
1249
+ `Invalid URL encoded data returned from route handler ${
1250
+ route.path
1251
+ } - expected URLSearchParams but got ${
1252
+ result?.constructor?.name || typeof result
1253
+ }`,
1254
+ )
1255
+ }
1256
+ return new Response(result, {
1257
+ headers: {
1258
+ 'content-type': 'application/x-www-form-urlencoded',
1259
+ },
1260
+ })
1261
+ }
1262
+
1263
+ if (route.type?.includes('text/plain')) {
1264
+ if (typeof result !== 'string') {
1265
+ throw new Error(
1266
+ `Invalid text returned from route handler ${
1267
+ route.path
1268
+ } - expected string but got ${
1269
+ result?.constructor?.name || typeof result
1270
+ }`,
1271
+ )
1272
+ }
1273
+ return new Response(result, {
1274
+ headers: {
1275
+ 'content-type': 'text/plain',
1276
+ },
1277
+ })
1278
+ }
1279
+ }
1210
1280
  return new Response(JSON.stringify(result ?? null, null, 2), {
1211
1281
  headers: {
1212
1282
  'content-type': 'application/json',