javascript - Next.js 14 Sunucu İşlemleri: Çerezleri Değiştirme Sorunu


Şu anda bir Next.js 14 uygulaması geliştiriyorum ve çerezleri değiştirme konusunda bir sorunla karşılaştım. Next.js dökümantasyonunda sunucu eylemleri ve çerez manipülasyonu ile ilgili adımları takip etmeme rağmen, çıkış işlemi sırasında çerezleri ayarlamaya çalıştığımda bir hata alıyorum. Hata mesajı şöyle diyor: "Çerezler yalnızca bir Sunucu Eylemi veya Rota İşleyicisinde değiştirilebilir."

İşte projemden ilgili kod bölümü:

// pwa/src/utils/globalActions.ts

'use server';

import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';

export const signout = async () => {
  cookies.set('token', '', { maxAge: -1 });
  cookies.set('refreshToken', '', { maxAge: -1 });
  cookies.set('user', '', { maxAge: -1 });
  return redirect('/auth/signin');
};
// pwa/src/utils/apiAxios.ts (This file is used by both client and server components)

import axios, {
    type AxiosResponse,
    type AxiosError,
    type AxiosInstance,
} from 'axios';
import { signout } from './globalActions';

const Axios = axios.create({
    baseURL: ENTRYPOINT,
    headers: {
        Accept: 'application/ld+json',
        'Content-Type': 'application/ld+json',
    },
});

Axios.interceptors.response.use(
    response => response,
    async (error: AxiosError<ApiResponseError>) => {
        if (error.status === 401 || error.response?.status === 401) {
            await signout();
        }

        return Promise.reject(error);
    },
);

Kullanımımı Next.js belgelerine göre kontrol ettim ve sunucu yürütme bağlamını ('use server';) kullandığımdan emin oldum. Yine de sorun devam ediyor. Next.js 14'ün sunucu aksiyonlarında çerezleri nasıl ele aldığıyla ilgili bir detayı kaçırıyor olabileceğimi veya gözden kaçırdığım özel bir yapılandırma adımı olabileceğini düşünüyorum.

Daha önce bu sorunla karşılaşan oldu mu, yoksa Next.js 14'te sunucu bileşenlerinde çerezleri doğru şekilde nasıl değiştireceğimiz konusunda herhangi bir fikir sahibi olan var mı? Her türlü yardım veya yönlendirme çok takdir edilecektir.

Cevap #1

Çerezler yalnızca bir Sunucu Eyleminde veya Rota İşleyicisinde değiştirilebilir.

bu mesaj, sunucu tarafında olmanız gerektiğini gösteriyor ancak bildiğim kadarıyla axios.interceptors.response, Axios sunucudan bir yanıt aldıktan hemen sonra ve Axios isteği tarafından döndürülen promise çözülmeden önce çağrılır.

yani sunucuda istek gerçekleştikten sonra cookies.set çağırıyorsun

Cevap #2

Dokümantasyona göre NextJS 13 veya daha yeni bir sürüm kullanıyorsanız çerezleri middleware içinde ayarlamanız gerekir.

Good to know: `cookies()` is a Dynamic Function whose returned values cannot
be known ahead of time. Using it in a layout or page will opt a route into
dynamic rendering at request time.

Ara katman ekledikten sonra çerezleri istediğiniz yerde okuyabilirsiniz ancak fonksiyonu "use server" olarak işaretlemeniz gerekir.

Daha iyi açıklayan bir YouTube videosu burada.

İşte bunu nasıl yaptığımı açıklama

signout özelliği için sayfayı /log-out adresine yönlendiriyorum ve middleware içinde mevcut URL’yi kontrol ediyorum. Eğer mevcut URL /log-out içeriyorsa tüm çerezleri siliyorum ve ardından ana sayfaya yönlendiriyorum.

import { NextResponse } from 'next/server';
import { NextRequest as NextRequestType } from 'next/server';

const ERROR_FALLBACK = "/log-out"

export async function middleware(req: NextRequestType) {
  let token = req.cookies.get("ACCESS_TOKEN");

  if (req.nextUrl.pathname.includes(ERROR_FALLBACK)) {
    token = undefined;
  }

  if (!token) {
    try {
      const initBody = {
        device: { osType: 'web' }
      };

      const { data: result } = await axios.post(`${process.env.NEXT_PUBLIC_BASE_URL}${Endpoints.INIT}`, initBody);
      const { token } = result.data;

      let response: NextResponse

      if (req.nextUrl.pathname.includes(ERROR_FALLBACK)) {
        response = NextResponse.redirect(new URL('/', req.url));
      } else {
        response = NextResponse.next();
      }

      response.cookies.set('ACCESS_TOKEN', encrypt(token, HashAlgorithm.AES), {
        maxAge: 25 * 365 * 24 * 60 * 60 * 1000
      });

      return response;
    } catch (error) {
      return NextResponse.redirect(new URL(ERROR_FALLBACK, req.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: "/((?!api|static|.*\\..*|_next).*)",
};

Mevcut URL eşleştirici ile uyuşuyorsa, bu middleware fonksiyonu sayfa yüklenmeden önce her seferinde çağrılır.

Matcher hakkında daha fazla bilgi için dokümantasyonu inceleyebilirsiniz.

Yukarıdaki kodun açıklaması

Benim backend uygulamamda her kullanıcının erişim token'ı var. Bu endpoint'i çağırarak token'ı alıyor. Middleware içinde çerezlerde token yoksa backend'ime bir istek gönderiyorum.

Eğer mevcut uç nokta benim FALLBACK URL’im içeriyorsa, onu güncelleyerek sıfırlıyorum.

Ama sizin durumunuzda çerezleri temizlemek istiyorsunuz. Bunu aşağıdaki gibi yapabilirsiniz.

  if (req.nextUrl.pathname.includes(ERROR_FALLBACK)) {
    let response: NextResponse = NextResponse.redirect(new URL('/auth/signin', req.url));
    response.cookies.delete('token');
    response.cookies.delete('refreshToken');
    response.cookies.delete('user');
    return response;
  }

Axios içinde yönlendirme nasıl yapılır?

UI'ya bir mesaj göndermeniz ve bunu istemci bileşeni içinde dinlemeniz gerekiyor. İşte bunu nasıl yapabileceğinize dair bir açıklama.

İlk olarak, bunun için bir mesaj ve dinleyici gönderen bir yardımcı işlev yazın.

import { EventEmitter } from 'events';

const emitter = new EventEmitter();

export const navigateTo = (path: string) => {
  emitter.emit('navigate', path);
};

export const onNavigate = (listener) => {
  emitter.on('navigate', listener);
};

Ve şimdi istemcide bu olayı dinleyen bir yardımcı bileşen yaz.

"use client"

import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { onNavigate } from '@/lib/Events';

export default function EventListener({ children }) {
  const router = useRouter();

  useEffect(() => {
    const handleNavigation = (path: string) => {
      router.replace(path);
    };

    onNavigate(handleNavigation);
  }, [router]);

  return children;
}

Yukarıdaki bileşeni oluşturduktan sonra, onu üst bileşeninize ekleyin. Benim durumumda bu bileşeni layout.tsx içine ekledim.

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <EventListener>
          {children}
        </EventListener>
        <Toaster />
      </body>
    </html>
  );
}

Artık signout() fonksiyonunuzu aşağıdaki gibi güncelleyebilirsiniz

export const signout = () => {
  navigateTo(WebLink.ERROR_FALLBACK);
};

Bundan sonra istediğiniz yerde signout fonksiyonunu çağırabilirsiniz.

Axios başlığına token nasıl eklenir?

Bileşeni veya işlevi "use server" olarak işaretlerseniz cookies öğesini istediğiniz yerde okuyabilirsiniz

"use server"

export async function getToken() {
  const a = await getCookieValue(COOKIE_KEY.TOKEN);
  return a;
}

async function getCookieValue(key: string) {
  return cookies().get(key)?.value;
}

Ve axios istek gönderici nesnesi

export const Requester = axios.create({
  baseURL: process.env.NEXT_PUBLIC_BASE_URL,
  timeout: 20000,
});

Requester.interceptors.request.use(
  async (config) => {
    const token = await getToken();
    if (token) {
      config.headers['token'] = token;
    }
    return config;
  },
  (error) => {
    console.warn(error);
    return Promise.reject(error);
  },
);

Requester.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    if (error.response?.data?.meta?.statusCode === 101) {
      endSession();
      return {};
    }

    if (error.response?.data?.meta?.statusCode === 104) {
      endSession();
      return {};
    }
    return Promise.reject(error);
  },
);

İlgili makaleler