Files
calendar/apps/web/app/api/mintlify-chat/message/route.ts
T
a7d87fc5be fix: Proxy Mintlify traffic through a Next.js API route (#25320)
* hotfix

* type fix and test fix

* update env example

* improvements

* more fix

* tada

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-11-21 18:43:09 +00:00

134 lines
3.5 KiB
TypeScript

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import {
validateChatMessage,
validateMintlifyConfig,
sanitizeResponseHeaders,
} from "@calcom/lib/server/mintlifyChatValidation";
/**
* POST /api/mintlify-chat/message
*
* Sends a message to a Mintlify chat topic and streams the response.
* This endpoint proxies requests to Mintlify's API while:
* - Keeping the API key secure on the server side
* - Validating and sanitizing user input
* - Preventing malicious payloads
*
* Expected body:
* {
* message: string,
* topicId: string
* }
*/
export async function POST(req: NextRequest) {
try {
// Validate configuration
const { apiKey, apiBaseUrl } = validateMintlifyConfig();
// Parse and validate request body
let body;
try {
body = await req.json();
} catch {
return NextResponse.json(
{ error: "Invalid JSON payload" },
{ status: 400 }
);
}
// Validate and sanitize the message payload
let message: string;
let topicId: string;
try {
const validated = validateChatMessage(body);
message = validated.message;
topicId = validated.topicId;
} catch (validationError) {
// For validation errors, return detailed message with 400
if (validationError instanceof Error) {
return NextResponse.json(
{ error: validationError.message },
{ status: 400 }
);
}
return NextResponse.json(
{ error: "Invalid request" },
{ status: 400 }
);
}
// Make request to Mintlify
const messageUrl = `${apiBaseUrl}/message`;
console.log("[Mintlify Chat Message] Fetching message endpoint", { messageUrl, topicId });
const queryResponse = await fetch(messageUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({ message, topicId }),
});
if (!queryResponse.ok) {
return NextResponse.json(
{ error: "Failed to send message" },
{ status: queryResponse.status }
);
}
if (!queryResponse.body) {
return NextResponse.json(
{ error: "No response body from Mintlify" },
{ status: 500 }
);
}
// Sanitize response headers before forwarding
const sanitizedHeaders = sanitizeResponseHeaders(queryResponse.headers);
// Stream the response back to the client
// We need to pipe the ReadableStream from Mintlify directly to the client
const stream = new ReadableStream({
async start(controller) {
const reader = queryResponse.body!.getReader();
try {
for (;;) {
const { done, value } = await reader.read();
if (done) {
controller.close();
break;
}
controller.enqueue(value);
}
} catch (error) {
console.error("[Mintlify Chat Message Stream]", error);
controller.error(error);
}
},
});
return new NextResponse(stream, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Transfer-Encoding": "chunked",
...sanitizedHeaders,
},
});
} catch {
// Return generic error message to avoid leaking internal details
// Validation errors are already handled above with specific messages
return NextResponse.json(
{ error: "Failed to send message. Please try again later." },
{ status: 500 }
);
}
}