feat: improve nextjs proxying mode
This commit is contained in:
@@ -3,80 +3,141 @@ import { createHash } from 'node:crypto';
|
||||
// with esm and nextjs (when using pages dir)
|
||||
import { NextResponse } from 'next/server.js';
|
||||
|
||||
type CreateNextRouteHandlerOptions = {
|
||||
type RouteHandlerOptions = {
|
||||
apiUrl?: string;
|
||||
};
|
||||
|
||||
export function createNextRouteHandler(
|
||||
options?: CreateNextRouteHandlerOptions,
|
||||
) {
|
||||
return async function POST(req: Request) {
|
||||
const apiUrl = options?.apiUrl ?? 'https://api.openpanel.dev';
|
||||
const headers = new Headers();
|
||||
const DEFAULT_API_URL = 'https://api.openpanel.dev';
|
||||
const SCRIPT_URL = 'https://openpanel.dev';
|
||||
const SCRIPT_PATH = '/op1.js';
|
||||
|
||||
const ip =
|
||||
req.headers.get('cf-connecting-ip') ??
|
||||
req.headers.get('x-forwarded-for')?.split(',')[0] ??
|
||||
req.headers.get('x-vercel-forwarded-for');
|
||||
headers.set('Content-Type', 'application/json');
|
||||
headers.set(
|
||||
'openpanel-client-id',
|
||||
req.headers.get('openpanel-client-id') ?? '',
|
||||
);
|
||||
headers.set('origin', req.headers.get('origin') ?? '');
|
||||
headers.set('User-Agent', req.headers.get('user-agent') ?? '');
|
||||
if (ip) {
|
||||
headers.set('openpanel-client-ip', ip);
|
||||
}
|
||||
function getClientHeaders(req: Request): Headers {
|
||||
const headers = new Headers();
|
||||
const ip =
|
||||
req.headers.get('cf-connecting-ip') ??
|
||||
req.headers.get('x-forwarded-for')?.split(',')[0] ??
|
||||
req.headers.get('x-vercel-forwarded-for');
|
||||
|
||||
try {
|
||||
const res = await fetch(`${apiUrl}/track`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(await req.json()),
|
||||
});
|
||||
return NextResponse.json(await res.text(), { status: res.status });
|
||||
} catch (e) {
|
||||
return NextResponse.json(e);
|
||||
}
|
||||
};
|
||||
headers.set('Content-Type', 'application/json');
|
||||
headers.set(
|
||||
'openpanel-client-id',
|
||||
req.headers.get('openpanel-client-id') ?? '',
|
||||
);
|
||||
|
||||
// Construct origin: browsers send Origin header for POST requests and cross-origin requests,
|
||||
// but not for same-origin GET requests. Fallback to constructing from request URL.
|
||||
const origin =
|
||||
req.headers.get('origin') ??
|
||||
(() => {
|
||||
const url = new URL(req.url);
|
||||
return `${url.protocol}//${url.host}`;
|
||||
})();
|
||||
headers.set('origin', origin);
|
||||
|
||||
headers.set('User-Agent', req.headers.get('user-agent') ?? '');
|
||||
if (ip) {
|
||||
headers.set('openpanel-client-ip', ip);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function createScriptHandler() {
|
||||
return async function GET(req: Request) {
|
||||
const url = new URL(req.url);
|
||||
const query = url.searchParams.toString();
|
||||
async function handleApiRoute(
|
||||
req: Request,
|
||||
apiUrl: string,
|
||||
apiPath: string,
|
||||
): Promise<NextResponse> {
|
||||
const headers = getClientHeaders(req);
|
||||
|
||||
if (!url.pathname.endsWith('op1.js')) {
|
||||
try {
|
||||
const res = await fetch(`${apiUrl}${apiPath}`, {
|
||||
method: req.method,
|
||||
headers,
|
||||
body:
|
||||
req.method === 'POST' ? JSON.stringify(await req.json()) : undefined,
|
||||
});
|
||||
|
||||
if (res.headers.get('content-type')?.includes('application/json')) {
|
||||
return NextResponse.json(await res.json(), { status: res.status });
|
||||
}
|
||||
return NextResponse.json(await res.text(), { status: res.status });
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to proxy request',
|
||||
message: e instanceof Error ? e.message : String(e),
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleScriptProxyRoute(req: Request): Promise<NextResponse> {
|
||||
const url = new URL(req.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
if (!pathname.endsWith(SCRIPT_PATH)) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
let scriptUrl = `${SCRIPT_URL}${SCRIPT_PATH}`;
|
||||
if (url.searchParams.size > 0) {
|
||||
scriptUrl += `?${url.searchParams.toString()}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(scriptUrl, {
|
||||
// @ts-ignore
|
||||
next: { revalidate: 86400 },
|
||||
});
|
||||
const text = await res.text();
|
||||
const etag = `"${createHash('md5')
|
||||
.update(scriptUrl + text)
|
||||
.digest('hex')}"`;
|
||||
|
||||
return new NextResponse(text, {
|
||||
headers: {
|
||||
'Content-Type': 'text/javascript',
|
||||
'Cache-Control': 'public, max-age=86400, stale-while-revalidate=86400',
|
||||
ETag: etag,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch script',
|
||||
message: e instanceof Error ? e.message : String(e),
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createRouteHandler(options?: RouteHandlerOptions) {
|
||||
const apiUrl = options?.apiUrl ?? DEFAULT_API_URL;
|
||||
|
||||
return async function handler(req: Request): Promise<NextResponse> {
|
||||
const url = new URL(req.url);
|
||||
const pathname = url.pathname;
|
||||
const method = req.method;
|
||||
|
||||
// Handle script proxy: GET /op1.js
|
||||
if (method === 'GET' && pathname.endsWith(SCRIPT_PATH)) {
|
||||
return handleScriptProxyRoute(req);
|
||||
}
|
||||
|
||||
const apiPathMatch = pathname.indexOf('/track');
|
||||
if (apiPathMatch === -1) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const scriptUrl = 'https://openpanel.dev/op1.js';
|
||||
try {
|
||||
const res = await fetch(scriptUrl, {
|
||||
// @ts-ignore
|
||||
next: { revalidate: 86400 },
|
||||
});
|
||||
const text = await res.text();
|
||||
const etag = `"${createHash('md5')
|
||||
.update(text + query)
|
||||
.digest('hex')}"`;
|
||||
return new NextResponse(text, {
|
||||
headers: {
|
||||
'Content-Type': 'text/javascript',
|
||||
'Cache-Control':
|
||||
'public, max-age=86400, stale-while-revalidate=86400',
|
||||
ETag: etag,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch script',
|
||||
message: e instanceof Error ? e.message : String(e),
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
const apiPath = pathname.substring(apiPathMatch);
|
||||
return handleApiRoute(req, apiUrl, apiPath);
|
||||
};
|
||||
}
|
||||
|
||||
export { createRouteHandler };
|
||||
|
||||
// const routeHandler = createRouteHandler();
|
||||
// export const GET = routeHandler;
|
||||
// export const POST = routeHandler;
|
||||
|
||||
Reference in New Issue
Block a user