reactjsnext.jsspotify

Using access token across different API routes


I'm developing a react/Next.js app that is always getting access token expired for following calls after the initial one

I have 2 files; the first file loads in all items from the Spotify API, if there's no access token it creates one, and then goes on to use the access token without error.

The second file is for the details of an item that's supposed to use the same access token if not expired and not null.

The issue is with the second call for details, it always seems to return an expired token message and fails.

I've even tried to hardcode the token into the get-details code and it still doesn't work

get all items file:

import { BASE_URL_MUSIC } from "@/shared/constants/urls";
import { getValidToken } from "../token/tokenManager";

export const GET = async (req: any) => {
  try {
    const data = await getAllAlbums();
    return new Response(JSON.stringify({ data }), { status: 200 });
  } catch (error) {
    console.error("Failed to get albums:", error);
    return new Response(JSON.stringify({ error: "Failed to get albums" }), {
      status: 500,
    });
  }
};

const getAllAlbums = async () => {
  const token = await getValidToken();

  const response = await fetch(
    `${BASE_URL_MUSIC}/browse/new-releases?limit=20&country=GB`,
    {
      method: "GET",
      headers: {
        Authorization: `Bearer ${token}`,
      },
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw { status: response.status, ...error };
  }

  return response.json();
};

get details file:

import { BASE_URL_MUSIC } from "@/shared/constants/urls";
import { getValidToken } from "../token/tokenManager";

export const GET = async (req: any) => {
  const { searchParams } = new URL(req.url);
  const id = searchParams.get("id");

  try {
    const data = await getAlbumDetails(id);
    return new Response(JSON.stringify({ data }), { status: 200 });
  } catch (error: any) {
    //ALWAYS STICKS HERE
    console.error("Error fetching album details:", error);
    if (error.status === 401) {
      console.log("Token expired, refreshing...");

      try {
        const token = await getValidToken();
        const data = await getAlbumDetails(id, token);
        return new Response(JSON.stringify({ data }), { status: 200 });
      } catch (tokenError) {
       //AND HERE
        console.error("Failed to refresh token:", tokenError);
        return new Response(
          JSON.stringify({
            error: "Failed to refresh token and get album details",
          }),
          { status: 500 }
        );
      }
    } else {
      return new Response(
        JSON.stringify({ error: "Failed to get album details" }),
        { status: 500 }
      );
    }
  }
};

const getAlbumDetails = async (id: any, token?: string) => {
  if (!token) {
    token = await getValidToken();
  }

  const response = await fetch(`${BASE_URL_MUSIC}/albums/${id}`, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  if (!response.ok) {
    const error = await response.json();
    throw { status: response.status, ...error };
  }

  return response.json();
};

getToken:

interface TokenResponse {
  access_token: string;
  expires_in: number;
}

export const getToken = async (): Promise<TokenResponse> => {
  const response = await fetch("http://localhost:3000/api/music/token/");
  const data = await response.json();

  if (!data.data || !data.data.access_token || !data.data.expires_in) {
    throw new Error("Invalid token response");
  }

  console.log("Token fetched:", data.data.access_token);

  return {
    access_token: data.data.access_token,
    expires_in: data.data.expires_in,
  };
};

tokenManager:

import { getToken } from "./getToken";

interface TokenResponse {
  access_token: string;
  expires_in: number;
}

let cachedToken: string | null = null;
let tokenExpiry: number | null = null;

export const getValidToken = async (): Promise<string> => {
  const now = Date.now();

  if (cachedToken && tokenExpiry && tokenExpiry > now) {
    console.log("Using cached token");
    return cachedToken;
  }

  console.log("Fetching new token...");
  const tokenResponse: TokenResponse = await getToken();
  cachedToken = tokenResponse.access_token;
  tokenExpiry = now + tokenResponse.expires_in * 1000;

  console.log("New token fetched", cachedToken);

  if (!cachedToken) {
    throw new Error("Failed to obtain a valid token");
  }

  return cachedToken;
};

Solution

  • To solve this I put all the code into one file, I suppose trying to emulate how I did it before with a node/express server.

    Once that was working I refactored the code into different files and routes to end up with the below.

    get-items/route.ts

    import { BASE_URL_MUSIC } from "@/shared/constants/urls";
    
    import { NextRequest } from "next/server";
    import { getValidToken } from "@/app/api/music/token/getToken";
    
    let token: string | null;
    
    export const GET = async (req: NextRequest) => {
      const data = await getAllAlbums();
      return new Response(JSON.stringify({ data }), { status: 200 });
    };
    
    const getAllAlbums = async () => {
      const token = await getValidToken();
    
      const response = await fetch(
        `${BASE_URL_MUSIC}/browse/new-releases?limit=20&country=GB`,
        {
          method: "GET",
          headers: {
            Authorization: `Bearer ${token}`,
          },
        }
      );
    
      const data = await response.json();
    
      if (!response.ok) {
        throw new Error(`Failed to fetch albums: ${data.error.message}`);
      }
    
      return data;
    };
    

    get-details:

    import { BASE_URL_MUSIC } from "@/shared/constants/urls";
    import { NextRequest } from "next/server";
    
    import { getValidToken } from "@/app/api/music/token/getToken";
    
    let id: string | null;
    let token: string | null;
    
    export const GET = async (req: NextRequest) => {
      const { searchParams } = new URL(req.url);
      id = searchParams.get("id") as string;
    
      const data = await getAlbumDetails(id);
      return new Response(JSON.stringify({ data }), { status: 200 });
    };
    
    const getAlbumDetails = async (id: string) => {
      const token = await getValidToken();
    
      const response = await fetch(`${BASE_URL_MUSIC}/albums/${id}`, {
        method: "GET",
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
    
      const album = await response.json();
    
      if (!response.ok) {
        throw new Error(`Failed to fetch album details: ${album.error.message}`);
      }
    
      return album;
    };
    

    getting and setting token logic getToken.ts

    const clientID = process.env.SPOTIFY_CLIENT_ID;
    const clientSecret = process.env.SPOTIFY_CLIENT_SECRET;
    
    let cachedToken: string | null = null;
    let tokenExpiry: number | null = null;
    
    const getToken = async (): Promise<{
      access_token: string;
      expires_in: number;
    }> => {
      const response = await fetch(`https://accounts.spotify.com/api/token`, {
        method: "POST",
        body: new URLSearchParams({
          grant_type: "client_credentials",
        }),
        headers: {
          Authorization: `Basic ${Buffer.from(
            `${clientID}:${clientSecret}`
          ).toString("base64")}`,
          "Content-Type": "application/x-www-form-urlencoded",
        },
      });
    
      if (!response.ok) {
        const error = await response.json();
        throw new Error(`Failed to fetch token: ${error.error_description}`);
      }
    
      const data = await response.json();
      return { access_token: data.access_token, expires_in: data.expires_in };
    };
    
    export const getValidToken = async (): Promise<string> => {
      const now = Date.now();
    
      if (cachedToken && tokenExpiry && tokenExpiry > now) {
        console.log("Token cached===", cachedToken);
    
        return cachedToken;
      }
      console.log("Token not cached===", cachedToken);
      const tokenResponse = await getToken();
      cachedToken = tokenResponse.access_token;
      tokenExpiry = now + tokenResponse.expires_in * 1000;
    
      return cachedToken;
    }
    

    ;

    The new code has no error logic in the route files (searching for 401s, etc.) Maybe that was a hindering thing.

    If anyone can elaborate on this, feel free