File size: 4,744 Bytes
6111b2b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | /**
* API: OpenAPI "Try It" Proxy
* POST — forwards a request to a local endpoint and returns the result
*/
import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
const ALLOWED_TRY_PATH_PREFIXES = ["/api/", "/v1/", "/v1beta/", "/a2a", "/.well-known/agent.json"];
const BLOCKED_FORWARD_HEADERS = new Set([
"connection",
"content-length",
"cookie",
"host",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
"x-forwarded-for",
"x-forwarded-host",
"x-forwarded-proto",
]);
const tryRequestSchema = z.object({
method: z
.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"])
.optional()
.default("GET"),
path: z
.string()
.min(1, "Path is required")
.startsWith("/", "Path must start with /")
.refine((value) => !value.startsWith("//"), "Path must be a same-origin path")
.refine(
(value) => ALLOWED_TRY_PATH_PREFIXES.some((prefix) => value.startsWith(prefix)),
"Path must target an OmniRoute API endpoint"
),
headers: z.record(z.string(), z.string()).optional().default({}),
body: z.any().optional(),
});
function getRequestOrigin(request: NextRequest) {
return request.nextUrl?.origin || new URL(request.url).origin;
}
function buildForwardHeaders(headers: Record<string, string>) {
const forwardHeaders: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
const normalizedKey = key.trim().toLowerCase();
if (!normalizedKey || BLOCKED_FORWARD_HEADERS.has(normalizedKey)) continue;
forwardHeaders[key] = value;
}
return forwardHeaders;
}
export async function POST(request: NextRequest) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const rawBody = await request.json();
const validation = validateBody(tryRequestSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { method, path, headers, body: reqBody } = validation.data;
const origin = getRequestOrigin(request);
const targetUrl = new URL(path, origin);
if (targetUrl.origin !== origin) {
return NextResponse.json({ error: "Path must be same-origin" }, { status: 400 });
}
const start = performance.now();
// Forward cookies/auth from the original request
const forwardHeaders = buildForwardHeaders(headers as Record<string, string>);
// Forward auth from the dashboard session
const cookie = request.headers.get("cookie");
if (cookie && !forwardHeaders["Cookie"]) {
forwardHeaders["Cookie"] = cookie;
}
if (reqBody && !forwardHeaders["Content-Type"]) {
forwardHeaders["Content-Type"] = "application/json";
}
const fetchOptions: RequestInit = {
method: method.toUpperCase(),
headers: forwardHeaders,
};
if (reqBody && method.toUpperCase() !== "GET") {
fetchOptions.body = typeof reqBody === "string" ? reqBody : JSON.stringify(reqBody);
}
const res = await fetch(targetUrl, fetchOptions);
const latencyMs = Math.round(performance.now() - start);
// Read response
const contentType = res.headers.get("content-type") || "";
let responseBody: any;
if (contentType.includes("application/json")) {
responseBody = await res.json();
} else {
const text = await res.text();
// Truncate very large responses
responseBody = text.length > 10000 ? text.slice(0, 10000) + "\n... (truncated)" : text;
}
// Collect response headers
const responseHeaders: Record<string, string> = {};
res.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
return NextResponse.json({
status: res.status,
statusText: res.statusText,
headers: responseHeaders,
body: responseBody,
latencyMs,
contentType,
});
} catch (error: any) {
return NextResponse.json(
{
status: 0,
statusText: "Network Error",
headers: {},
body: { error: sanitizeErrorMessage(error) || "Request failed" },
latencyMs: 0,
contentType: "application/json",
},
{ status: 200 } // Return 200 so the frontend can display the error
);
}
}
|