SSR (Remix) onError: how to redirect to login from a link?

Hi there,

I am looking to redirect the user to the login page when a GQL API call throw an UNAUTHENTICATED error.

Note that my stack is Remix (SSR) and GQL is running on the server side.

Here is my code (redirect not working):

import https from "https";
import { onError } from "@apollo/client/link/error";
import { fromPromise } from "@apollo/client";
import { redirect, redirectDocument } from "@remix-run/node";
import { authTokenCookie } from "~/cookies.server";

async function authenticate(request: Request) {
  const payload: { token: string; isLoggedIn: boolean } = await fetch(
    `https://domain.tld/api/remix/auth-token`,
    {
      agent: new https.Agent({
        rejectUnauthorized: process.env.NODE_ENV !== "development",
      }),
      headers: {
        cookie: request.headers.get("cookie"),
      },
    } as RequestInit,
  )
    .then((response) => response.json())
    .catch((error) => {
      console.error(error);
      return null;
    });

  if (!payload.isLoggedIn) {
    // Redirect to login form.
    throw redirectDocument(
      `https://domain.tld/login?_target_path=${new URL(request.url).href}`,
    );
  }

  // Update the token cookie for future requests.
  throw redirect(request.url, {
    headers: {
      "Set-Cookie": await authTokenCookie.serialize(payload.token),
    },
  });
}

async function createClient(request: Request) {
  const cookieHeader = request.headers.get("Cookie");
  const authToken = cookieHeader ? await authTokenCookie.parse(cookieHeader) : null;

  if (!authToken) {
    throw await authenticate(request);
  }

  const httpLink = createHttpLink({
    uri: "https://domain.tld/api/gql",
    fetch, // Import from node-fetch, don't know why that is not working without this declaration...
    fetchOptions: {
      agent: new https.Agent({
        rejectUnauthorized: process.env.NODE_ENV !== "development",
      }),
    },
  });

  const authLink = setContext((_, { headers }) => {
    return {
      headers: {
        ...headers,
        cookie: cookieHeader ? await sessionCookie.parse(cookieHeader) : null,
        authorization: `Bearer ${authToken}`,
      },
    };
  });

  const errorLink = onError(({ graphQLErrors, operation, forward }) => {
    const isAuthenticationError = graphQLErrors?.some((error) => {
      return error.extensions.code === "UNAUTHENTICATED";
    });

    // If it's an unauthenticated error, we refresh the token cookie (if the user is logged in) or we redirect the user to the login form.
    if (isAuthenticationError) {
      // THIS REDIRECT IS NOT WORKING...
      return fromPromise(authenticate(request))
        .filter((value) => Boolean(value))
        .flatMap(() => {
          return forward(operation);
        });
    }

    // If that's another error, we log it to console.
    if (graphQLErrors) {
      graphQLErrors.forEach((error) => {
        console.error(
          `[GraphQL error]: ${error.message}`,
          error,
        );
      });
    }
  });

  const retryLink = new RetryLink({
    delay: {
      initial: 300,
      max: 500,
      jitter: true,
    },
    attempts: {
      max: 3,
      retryIf: (error, _operation) => !!error,
    },
  });

  const links = [authLink, errorLink, retryLink, httpLink];

  return new ApolloClient({
    ssrMode: true,
    cache: createApolloCache(),
    link: from(links),
    connectToDevTools: process.env.NODE_ENV === "development",
  });
}

PS: I tried to add a redirect: "follow" to fetchOptions but that is not working neither.

Is it possible to make a http redirect from an Apollo link?

Thanks for your help!
David