fix(frontend) : Update server-side mutator to bypass proxy (#10523)

This PR helps us bypass the proxy server in server-side requests,
allowing us to directly send requests to the backend and reduce latency.

### Changes 🏗️
- Introduced server-side detection to dynamically set the base URL for
API requests.
- Added error handling for server-side requests to log failures and
throw errors appropriately.
- Updated header management to include authentication tokens when
applicable.

### Checklist 📋
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] All E2E tests are working.
- [x] I have manually checked the server-side and client-side
components, and both are working perfectly.
This commit is contained in:
Abhimanyu Yadav
2025-08-04 17:06:25 +05:30
committed by GitHub
parent 5dbc3a7d39
commit e043e4989b
11 changed files with 70 additions and 21 deletions

View File

@@ -1,5 +1,6 @@
"use client";
import { isServerSide } from "@/lib/utils/is-server-side";
import { useEffect, useState } from "react";
export default function AuthErrorPage() {
@@ -9,7 +10,7 @@ export default function AuthErrorPage() {
useEffect(() => {
// This code only runs on the client side
if (typeof window !== "undefined") {
if (!isServerSide()) {
const hash = window.location.hash.substring(1); // Remove the leading '#'
const params = new URLSearchParams(hash);

View File

@@ -1,7 +1,23 @@
import {
createRequestHeaders,
getServerAuthToken,
} from "@/lib/autogpt-server-api/helpers";
import { isServerSide } from "@/lib/utils/is-server-side";
const FRONTEND_BASE_URL =
process.env.NEXT_PUBLIC_FRONTEND_BASE_URL || "http://localhost:3000";
const API_PROXY_BASE_URL = `${FRONTEND_BASE_URL}/api/proxy`; // Sending request via nextjs Server
const getBaseUrl = (): string => {
if (!isServerSide()) {
return API_PROXY_BASE_URL;
} else {
return (
process.env.NEXT_PUBLIC_AGPT_SERVER_BASE_URL || "http://localhost:8006"
);
}
};
const getBody = <T>(c: Response | Request): Promise<T> => {
const contentType = c.headers.get("content-type");
@@ -30,11 +46,12 @@ export const customMutator = async <T = any>(
| "DELETE"
| "PATCH";
const data = requestOptions.body;
const headers: Record<string, string> = {
let headers: Record<string, string> = {
...((requestOptions.headers as Record<string, string>) || {}),
};
const isFormData = data instanceof FormData;
const contentType = isFormData ? "multipart/form-data" : "application/json";
// Currently, only two content types are handled here: application/json and multipart/form-data
if (!isFormData && data && !headers["Content-Type"]) {
@@ -45,13 +62,41 @@ export const customMutator = async <T = any>(
? "?" + new URLSearchParams(params).toString()
: "";
const response = await fetch(`${API_PROXY_BASE_URL}${url}${queryString}`, {
const baseUrl = getBaseUrl();
// The caching in React Query in our system depends on the url, so the base_url could be different for the server and client sides.
const fullUrl = `${baseUrl}${url}${queryString}`;
if (isServerSide()) {
try {
const token = await getServerAuthToken();
const authHeaders = createRequestHeaders(token, !!data, contentType);
headers = { ...headers, ...authHeaders };
} catch (error) {
console.warn("Failed to get server auth token:", error);
}
}
const response = await fetch(fullUrl, {
...requestOptions,
method,
headers,
body: data,
});
// Error handling for server-side requests
// We do not need robust error handling for server-side requests; we only need to log the error message and throw the error.
// What happens if the server-side request fails?
// 1. The error will be logged in the terminal, then.
// 2. The error will be thrown, so the cached data for this particular queryKey will be empty, then.
// 3. The client-side will send the request again via the proxy. If it fails again, the error will be handled on the client side.
// 4. If the request succeeds on the server side, the data will be cached, and the client will use it instead of sending a request to the proxy.
if (!response.ok && isServerSide()) {
console.error("Request failed on server side", response);
throw new Error(`Request failed with status ${response.status}`);
}
const response_data = await getBody<T>(response);
return {

View File

@@ -1,5 +1,6 @@
"use client";
import { cn } from "@/lib/utils";
import { isServerSide } from "@/lib/utils/is-server-side";
import { useEffect, useRef, useState } from "react";
export interface TurnstileProps {
@@ -31,7 +32,7 @@ export function Turnstile({
// Load the Turnstile script
useEffect(() => {
if (typeof window === "undefined" || !shouldRender) return;
if (isServerSide() || !shouldRender) return;
// Skip if already loaded
if (window.turnstile) {

View File

@@ -1,3 +1,4 @@
import { isServerSide } from "@/lib/utils/is-server-side";
import { debounce } from "lodash";
import { useCallback, useEffect, useRef, useState } from "react";
@@ -24,7 +25,7 @@ export const useInfiniteScroll = ({
const [isLoading, setIsLoading] = useState(false);
const handleScroll = useCallback(() => {
if (containerRef.current && typeof window !== "undefined") {
if (containerRef.current && !isServerSide()) {
const container = containerRef.current;
const { bottom } = container.getBoundingClientRect();
const { innerHeight } = window;

View File

@@ -1,6 +1,7 @@
import { useState, useCallback, useEffect } from "react";
import { verifyTurnstileToken } from "@/lib/turnstile";
import { BehaveAs, getBehaveAs } from "@/lib/utils";
import { isServerSide } from "@/lib/utils/is-server-side";
interface UseTurnstileOptions {
action?: string;
@@ -79,12 +80,7 @@ export function useTurnstile({
setError(null);
// Only reset the actual Turnstile widget if it exists and shouldRender is true
if (
shouldRender &&
typeof window !== "undefined" &&
window.turnstile &&
widgetId
) {
if (shouldRender && !isServerSide() && window.turnstile && widgetId) {
try {
window.turnstile.reset(widgetId);
} catch (err) {

View File

@@ -66,8 +66,9 @@ import type {
UserPasswordCredentials,
UsersBalanceHistoryResponse,
} from "./types";
import { isServerSide } from "../utils/is-server-side";
const isClient = typeof window !== "undefined";
const isClient = !isServerSide();
export default class BackendAPI {
private baseUrl: string;

View File

@@ -1,5 +1,6 @@
"use client";
import { isServerSide } from "../utils/is-server-side";
import BackendAPI from "./client";
import React, { createContext, useMemo } from "react";
@@ -19,10 +20,7 @@ export function BackendAPIProvider({
}): React.ReactNode {
const api = useMemo(() => new BackendAPI(), []);
if (
process.env.NEXT_PUBLIC_BEHAVE_AS == "LOCAL" &&
typeof window !== "undefined"
) {
if (process.env.NEXT_PUBLIC_BEHAVE_AS == "LOCAL" && !isServerSide()) {
window.api = api; // Expose the API globally for debugging purposes
}

View File

@@ -1,5 +1,6 @@
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { Key, storage } from "@/services/storage/local-storage";
import { isServerSide } from "../utils/is-server-side";
export class ApiError extends Error {
public status: number;
@@ -179,7 +180,7 @@ function isAuthenticationError(
}
function isLogoutInProgress(): boolean {
if (typeof window === "undefined") return false;
if (isServerSide()) return false;
try {
// Check if logout was recently triggered

View File

@@ -1,6 +1,7 @@
import { type CookieOptions } from "@supabase/ssr";
import { SupabaseClient } from "@supabase/supabase-js";
import { Key, storage } from "@/services/storage/local-storage";
import { isServerSide } from "../utils/is-server-side";
export const PROTECTED_PAGES = [
"/monitor",
@@ -82,7 +83,7 @@ export function setupSessionEventListeners(
onFocus: () => void,
onStorageChange: (e: StorageEvent) => void,
): EventListeners {
if (typeof window === "undefined" || typeof document === "undefined") {
if (isServerSide() || typeof document === "undefined") {
return { cleanup: () => {} };
}

View File

@@ -0,0 +1,3 @@
export const isServerSide = (): boolean => {
return typeof window === "undefined";
};

View File

@@ -1,3 +1,4 @@
import { isServerSide } from "@/lib/utils/is-server-side";
import * as Sentry from "@sentry/nextjs";
export enum Key {
@@ -8,7 +9,7 @@ export enum Key {
}
function get(key: Key) {
if (typeof window === "undefined") {
if (isServerSide()) {
Sentry.captureException(new Error("Local storage is not available"));
return;
}
@@ -21,7 +22,7 @@ function get(key: Key) {
}
function set(key: Key, value: string) {
if (typeof window === "undefined") {
if (isServerSide()) {
Sentry.captureException(new Error("Local storage is not available"));
return;
}
@@ -29,7 +30,7 @@ function set(key: Key, value: string) {
}
function clean(key: Key) {
if (typeof window === "undefined") {
if (isServerSide()) {
Sentry.captureException(new Error("Local storage is not available"));
return;
}