Browse Source

fix url

master
mortezaei 2 months ago
parent
commit
bc026a5afe
  1. 303
      src/app/api/proxy/route.ts
  2. 6
      src/components/questions/question-answer-storage.tsx
  3. 41
      src/lib/http.ts

303
src/app/api/proxy/route.ts

@ -1,303 +0,0 @@
import type { NextRequest } from "next/server";
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
const PROXY_PATH_PARAM = "__proxyPath";
const REQUEST_HEADERS_TO_FORWARD = [
"accept",
"accept-language",
"authorization",
"content-type",
"x-csrf-token",
"x-csrftoken",
"x-requested-with",
"x-xsrf-token",
];
const RESPONSE_HEADERS_TO_DROP = [
"connection",
"content-encoding",
"content-length",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
];
const MAX_LOG_BODY_LENGTH = 10_000;
const shouldLogProxy =
process.env.LOG_API_PROXY === "true" ||
(process.env.LOG_API_PROXY !== "false" &&
process.env.NODE_ENV !== "production");
class ProxyError extends Error {
constructor(
message: string,
readonly status: number,
) {
super(message);
}
}
function getApiBaseUrl() {
const apiBaseUrl =
process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL;
if (!apiBaseUrl) {
throw new ProxyError(
"API_BASE_URL or NEXT_PUBLIC_API_BASE_URL is required",
500,
);
}
try {
return new URL(apiBaseUrl);
} catch {
throw new ProxyError("API base URL is invalid", 500);
}
}
function getProxyPath(request: NextRequest) {
const proxyPath = request.nextUrl.searchParams.get(PROXY_PATH_PARAM);
if (!proxyPath) {
throw new ProxyError("Proxy path is required", 400);
}
if (/^[a-z][a-z\d+\-.]*:/i.test(proxyPath) || proxyPath.startsWith("//")) {
throw new ProxyError("Proxy path must be relative", 400);
}
return proxyPath.startsWith("/") ? proxyPath : `/${proxyPath}`;
}
function getTargetUrl(request: NextRequest) {
const targetUrl = getApiBaseUrl();
const proxyUrl = new URL(getProxyPath(request), targetUrl.origin);
const basePath = targetUrl.pathname.replace(/\/$/, "");
targetUrl.pathname = `${basePath}${proxyUrl.pathname}`;
const searchParams = new URLSearchParams(proxyUrl.search);
request.nextUrl.searchParams.forEach((value, key) => {
if (key !== PROXY_PATH_PARAM) {
searchParams.append(key, value);
}
});
targetUrl.search = searchParams.toString();
return targetUrl;
}
function getRequestHeaders(request: NextRequest, targetUrl: URL) {
const headers = new Headers();
const authKey = process.env.NEXT_PUBLIC_AUTH_KEY;
for (const header of REQUEST_HEADERS_TO_FORWARD) {
const value = request.headers.get(header);
if (value) {
headers.set(header, value);
}
}
if (authKey) {
headers.set("authorization", `token ${authKey}`);
}
headers.set("accept-encoding", "identity");
headers.set("http_x_user_language", "en");
headers.set("origin", targetUrl.origin);
headers.set("platform", "android");
headers.set("referer", `${targetUrl.origin}/`);
headers.set("user-agent", "dart:io");
return headers;
}
function getResponseHeaders(upstreamHeaders: Headers) {
const headers = new Headers(upstreamHeaders);
for (const header of RESPONSE_HEADERS_TO_DROP) {
headers.delete(header);
}
const setCookieHeaders =
(
upstreamHeaders as Headers & { getSetCookie?: () => string[] }
).getSetCookie?.() ?? [];
if (setCookieHeaders.length > 0) {
headers.delete("set-cookie");
for (const cookie of setCookieHeaders) {
headers.append("set-cookie", cookie);
}
}
return headers;
}
function getBodyLogValue(body: ArrayBuffer | undefined, contentType?: string) {
if (!body || body.byteLength === 0) {
return null;
}
if (contentType && !isTextContentType(contentType)) {
return `[${body.byteLength} bytes; ${contentType}]`;
}
const text = new TextDecoder().decode(body);
if (text.length <= MAX_LOG_BODY_LENGTH) {
return parseJsonForLog(text, text);
}
return `${text.slice(0, MAX_LOG_BODY_LENGTH)}... [truncated ${text.length - MAX_LOG_BODY_LENGTH} chars]`;
}
function isTextContentType(contentType: string) {
return (
contentType.includes("application/json") ||
contentType.includes("application/problem+json") ||
contentType.startsWith("text/") ||
contentType.includes("+json") ||
contentType.includes("+xml")
);
}
function parseJsonForLog(text: string, fallback: string) {
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
function headersToObject(headers: Headers) {
return Object.fromEntries(headers.entries());
}
function logProxyRequest(
request: NextRequest,
targetUrl: URL,
requestHeaders: Headers,
requestBody: ArrayBuffer | undefined,
) {
if (!shouldLogProxy) {
return;
}
writeProxyLog("request", {
incomingRequest: {
method: request.method,
url: request.url,
nextUrl: request.nextUrl.toString(),
headers: headersToObject(request.headers),
body: getBodyLogValue(
requestBody,
request.headers.get("content-type") ?? undefined,
),
},
upstreamRequest: {
method: request.method,
url: targetUrl.toString(),
headers: headersToObject(requestHeaders),
body: getBodyLogValue(
requestBody,
requestHeaders.get("content-type") ?? undefined,
),
},
payload: getBodyLogValue(
requestBody,
request.headers.get("content-type") ?? undefined,
),
});
}
function logProxyResponse(
upstreamResponse: Response,
responseBody: ArrayBuffer,
) {
if (!shouldLogProxy) {
return;
}
writeProxyLog("response", {
status: upstreamResponse.status,
statusText: upstreamResponse.statusText,
headers: headersToObject(upstreamResponse.headers),
body: getBodyLogValue(
responseBody,
upstreamResponse.headers.get("content-type") ?? undefined,
),
response: {
status: upstreamResponse.status,
statusText: upstreamResponse.statusText,
headers: headersToObject(upstreamResponse.headers),
body: getBodyLogValue(
responseBody,
upstreamResponse.headers.get("content-type") ?? undefined,
),
},
});
}
function writeProxyLog(label: string, value: unknown) {
console.log(`[api-proxy] ${label} ${JSON.stringify(value)}`);
}
async function proxyRequest(request: NextRequest) {
try {
const targetUrl = getTargetUrl(request);
const requestBody =
request.method === "GET" || request.method === "HEAD"
? undefined
: await request.arrayBuffer();
const requestHeaders = getRequestHeaders(request, targetUrl);
logProxyRequest(request, targetUrl, requestHeaders, requestBody);
const upstreamResponse = await fetch(targetUrl, {
method: request.method,
headers: requestHeaders,
body: requestBody,
cache: "no-store",
});
const responseBody = await upstreamResponse.arrayBuffer();
logProxyResponse(upstreamResponse, responseBody);
return new Response(responseBody, {
status: upstreamResponse.status,
statusText: upstreamResponse.statusText,
headers: getResponseHeaders(upstreamResponse.headers),
});
} catch (error) {
if (error instanceof ProxyError) {
return Response.json({ error: error.message }, { status: error.status });
}
console.error("API proxy request failed", error);
return Response.json(
{ error: "API proxy request failed" },
{ status: 502 },
);
}
}
export {
proxyRequest as DELETE,
proxyRequest as GET,
proxyRequest as HEAD,
proxyRequest as OPTIONS,
proxyRequest as PATCH,
proxyRequest as POST,
proxyRequest as PUT,
};

6
src/components/questions/question-answer-storage.tsx

@ -243,11 +243,7 @@ function writeStoredAnswers(
} }
function getKeepalivePatchUrl(slug: string) { function getKeepalivePatchUrl(slug: string) {
const searchParams = new URLSearchParams({
[PROXY_PATH_PARAM]: `/api/marriage/sections/${pathParam(slug)}/data/`,
});
return `/api/proxy?${searchParams.toString()}`;
return `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/marriage/sections/${pathParam(slug)}/data/`;
} }
export function QuestionAnswersProvider({ export function QuestionAnswersProvider({

41
src/lib/http.ts

@ -1,47 +1,10 @@
import axios, { type InternalAxiosRequestConfig } from "axios";
const PROXY_PATH_PARAM = "__proxyPath";
import axios from "axios";
export const http = axios.create({ export const http = axios.create({
baseURL: "/api/proxy",
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
headers: { headers: {
Accept: "application/json", Accept: "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
withCredentials: true, withCredentials: true,
}); });
function isAbsoluteUrl(url: string) {
return /^[a-z][a-z\d+\-.]*:\/\//i.test(url);
}
function withProxyPathParam(
params: InternalAxiosRequestConfig["params"],
proxyPath: string,
) {
if (params instanceof URLSearchParams) {
const nextParams = new URLSearchParams(params);
nextParams.set(PROXY_PATH_PARAM, proxyPath);
return nextParams;
}
if (typeof params === "string") {
const nextParams = new URLSearchParams(params);
nextParams.set(PROXY_PATH_PARAM, proxyPath);
return nextParams;
}
return {
...(params && typeof params === "object" ? params : {}),
[PROXY_PATH_PARAM]: proxyPath,
};
}
http.interceptors.request.use((config) => {
if (config.url && !isAbsoluteUrl(config.url)) {
config.params = withProxyPathParam(config.params, config.url);
config.url = "";
}
return config;
});
Loading…
Cancel
Save