From 4d65b9c07bc828a1f3c5e61ee13935065d655f66 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Tue, 23 Jul 2024 10:28:54 -0400 Subject: [PATCH] fix: increase max duration to 5 minutes for rest endpoints (#1177) --- .../ui/app/src/api-playground/types/proxy.ts | 56 ++++++++------ .../src/api-playground/types/serializable.ts | 64 +++++++++------- packages/ui/app/src/index.ts | 1 + .../src/pages/api/fern-docs/proxy/file.ts | 13 +++- .../src/pages/api/fern-docs/proxy/rest.ts | 74 ++++++++++--------- .../src/pages/api/fern-docs/proxy/stream.ts | 9 ++- 6 files changed, 127 insertions(+), 90 deletions(-) diff --git a/packages/ui/app/src/api-playground/types/proxy.ts b/packages/ui/app/src/api-playground/types/proxy.ts index 07f4e747ff..959d6cfd83 100644 --- a/packages/ui/app/src/api-playground/types/proxy.ts +++ b/packages/ui/app/src/api-playground/types/proxy.ts @@ -1,27 +1,39 @@ -import { JsonVariant } from "./jsonVariant"; -import { SerializableFile, SerializableFormDataEntryValue } from "./serializable"; +import { z } from "zod"; +import { SerializableFileSchema, SerializableFormDataEntryValueSchema, SerializableJsonSchema } from "./serializable"; + +export const SerializableFormDataSchema = z.object({ + type: z.literal("form-data"), + value: z.record(SerializableFormDataEntryValueSchema), +}); + +export type SerializableFormData = z.infer; + +export const SerializableOctetStreamSchema = z.object({ + type: z.literal("octet-stream"), + value: SerializableFileSchema.optional(), +}); + +export const SerializableBodySchema = z.union([ + SerializableJsonSchema, + SerializableFormDataSchema, + SerializableOctetStreamSchema, +]); + +export const ProxyRequestSchema = z.object({ + url: z.string(), + method: z.string(), + headers: z.record(z.string()), + body: SerializableBodySchema.optional(), + stream: z.boolean().optional(), + streamTerminator: z.string().optional(), +}); + +export type ProxyRequest = z.infer; export declare namespace ProxyRequest { - export interface SerializableFormData { - type: "form-data"; - value: Record; - } - - export interface SerializableOctetStream { - type: "octet-stream"; - value: SerializableFile | undefined; - } - - export type SerializableBody = JsonVariant | SerializableFormData | SerializableOctetStream; -} - -export interface ProxyRequest { - url: string; - method: string; - headers: Record; - body: ProxyRequest.SerializableBody | undefined; - stream?: boolean; - streamTerminator?: string; + export type SerializableOctetStream = z.infer; + export type SerializableBody = z.infer; + export type ProxyRequest = z.infer; } export declare namespace ProxyResponse { diff --git a/packages/ui/app/src/api-playground/types/serializable.ts b/packages/ui/app/src/api-playground/types/serializable.ts index 993e8db009..24e53ac6af 100644 --- a/packages/ui/app/src/api-playground/types/serializable.ts +++ b/packages/ui/app/src/api-playground/types/serializable.ts @@ -1,32 +1,42 @@ -export interface SerializableFile { - readonly name: string; - readonly lastModified: number; - readonly size: number; - readonly type: string; - readonly dataUrl: string; // base64-encoded -} +import { z } from "zod"; -export declare namespace SerializableFormDataEntryValue { - interface SingleFile { - type: "file"; - value: SerializableFile | undefined; - } +export const SerializableFileSchema = z + .object({ + name: z.string(), + lastModified: z.number(), + size: z.number(), + type: z.string(), + dataUrl: z.string({ description: "base64-encoded" }), + }) + .readonly(); - interface MultipleFiles { - type: "fileArray"; - value: SerializableFile[]; - } +export type SerializableFile = z.infer; - interface Json { - type: "json"; - value: unknown; +export const SerializableSingleFileSchema = z.object({ + type: z.literal("file"), + value: SerializableFileSchema.optional(), +}); - // if contentType is not provided, assume stringified JSON. Otherwise, use the provided contentType as a Blob type - contentType: string | undefined; - } -} +export const SerializableMultipleFilesSchema = z.object({ + type: z.literal("fileArray"), + value: z.array(SerializableFileSchema), +}); -export type SerializableFormDataEntryValue = - | SerializableFormDataEntryValue.Json - | SerializableFormDataEntryValue.SingleFile - | SerializableFormDataEntryValue.MultipleFiles; +export const SerializableJsonSchema = z.object({ + type: z.literal("json"), + value: z.unknown(), + contentType: z + .string({ + description: + "if contentType is not provided, assume stringified JSON. Otherwise, use the provided contentType as a Blob type", + }) + .optional(), +}); + +export const SerializableFormDataEntryValueSchema = z.union([ + SerializableSingleFileSchema, + SerializableMultipleFilesSchema, + SerializableJsonSchema, +]); + +export type SerializableFormDataEntryValue = z.infer; diff --git a/packages/ui/app/src/index.ts b/packages/ui/app/src/index.ts index 5765941991..09e597b8f7 100644 --- a/packages/ui/app/src/index.ts +++ b/packages/ui/app/src/index.ts @@ -1,5 +1,6 @@ export { type CustomerAnalytics } from "./analytics/types"; export { Stream } from "./api-playground/Stream"; +export { ProxyRequestSchema } from "./api-playground/types"; export type { ProxyRequest, ProxyResponse } from "./api-playground/types"; export { DEFAULT_FEATURE_FLAGS } from "./atoms"; export type { DocsProps, FeatureFlags } from "./atoms"; diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/proxy/file.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/proxy/file.ts index df2aa44501..d993be8e23 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/proxy/file.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/proxy/file.ts @@ -1,10 +1,17 @@ -import { ProxyRequest } from "@fern-ui/ui"; +import { ProxyRequestSchema } from "@fern-ui/ui"; import { NextRequest, NextResponse } from "next/server"; import { buildRequestBody } from "./rest"; +/** + * Note: edge functions must return a response within 25 seconds. + * + * This function is used to return the response directly from the proxied request, and is useful for file downloads. + * + * TODO: this should be rewritten as a node.js serverless function to avoid the 25 second limit, since file downloads can take a much longer time. + */ + export const runtime = "edge"; export const dynamic = "force-dynamic"; -export const maxDuration = 60 * 5; // 5 minutes export default async function POST(req: NextRequest): Promise> { if (req.method !== "POST" && req.method !== "OPTIONS") { @@ -27,7 +34,7 @@ export default async function POST(req: NextRequest): Promise { if (dataUrl.startsWith("http:") || dataUrl.startsWith("https:")) { @@ -108,31 +117,29 @@ export async function buildRequestBody(body: ProxyRequest.SerializableBody | und } } -export default async function POST(req: NextRequest): Promise> { +export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise { if (req.method !== "POST" && req.method !== "OPTIONS") { - return new NextResponse(null, { status: 405 }); + return res.status(405).send(null); } - const origin = req.headers.get("Origin"); + const origin = req.headers.origin; if (origin == null) { - return new NextResponse(null, { status: 400 }); + return res.status(400).send(null); } - const corsHeaders = { - "Access-Control-Allow-Origin": origin, - "Access-Control-Allow-Methods": "POST", - "Access-Control-Allow-Headers": "Content-Type", - }; + res.setHeader("Access-Control-Allow-Origin", origin); + res.setHeader("Access-Control-Allow-Methods", "POST"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { - return new NextResponse(null, { status: 204, headers: corsHeaders }); + return res.status(204).send(null); } // eslint-disable-next-line no-console console.log("Starting proxy request to", req.url); try { - const proxyRequest = (await req.json()) as ProxyRequest; + const proxyRequest = ProxyRequestSchema.parse(req.body); const requestBody = await buildRequestBody(proxyRequest.body); const headers = new Headers(proxyRequest.headers); @@ -169,26 +176,23 @@ export default async function POST(req: NextRequest): Promise( - { - response: { - headers: Object.fromEntries(responseHeaders.entries()), - ok: response.ok, - redirected: response.redirected, - status: response.status, - statusText: response.statusText, - type: response.type, - url: response.url, - body, - }, - time: endTime - startTime, - size: responseHeaders.get("Content-Length"), + res.status(200).json({ + response: { + headers: Object.fromEntries(responseHeaders.entries()), + ok: response.ok, + redirected: response.redirected, + status: response.status, + statusText: response.statusText, + type: response.type, + url: response.url, + body, }, - { status: 200, headers: corsHeaders }, - ); + time: endTime - startTime, + size: responseHeaders.get("Content-Length"), + }); } catch (err) { // eslint-disable-next-line no-console console.error(err); - return new NextResponse(null, { status: 500, headers: corsHeaders }); + return res.status(500).send(null); } } diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/proxy/stream.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/proxy/stream.ts index f3f1f6361f..d2eff5245f 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/proxy/stream.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/proxy/stream.ts @@ -1,9 +1,12 @@ -import { ProxyRequest } from "@fern-ui/ui"; +import { ProxyRequestSchema } from "@fern-ui/ui"; import { NextRequest, NextResponse } from "next/server"; +/** + * Note: edge functions must return a response within 25 seconds. + */ + export const runtime = "edge"; export const dynamic = "force-dynamic"; -export const maxDuration = 60 * 5; // 5 minutes export const supportsResponseStreaming = true; export default async function POST(req: NextRequest): Promise> { @@ -26,7 +29,7 @@ export default async function POST(req: NextRequest): Promise