clerk / t3-turbo-and-clerk

A t3 Turbo starter with Clerk as the auth provider.
https://clerk.dev
1.03k stars 75 forks source link

Question: Why do I get `x-clerk-auth-status` error? #61

Closed magnusrodseth closed 1 year ago

magnusrodseth commented 1 year ago

I have set up a pretty basic repository with Next.js, tRPC, and Clerk. Clerk generally works like a charm 🫶🏼

I have now set up some middleware to redirect the user if they're not an administrator of the service.

I now get an error that looks like the following:

TRPCClientError: Cannot read properties of undefined (reading 'x-clerk-auth-status')
    at TRPCClientError.from (transformResult-6fb67924.mjs?aa48:4:1)
    at eval (httpBatchLink.mjs?2c75:190:40)

Please see the screenshot for reference.

Screenshot 2023-03-01 at 17 26 56

I am aware that I provide very little information here, and I apologize. I am hoping that we can have a discussion about the origin of this error could be and that this discussion could help me debug the issue further. 😁

EDIT: Note that this header error affects all procedures defined using tRPC.

perkinsjr commented 1 year ago

Can you send me the code you are attempting? Are you using the latest @clerk/nextjs package ?

magnusrodseth commented 1 year ago

I'm attaching some relevant snippets:

packages/api/src/context.ts

import { prisma } from "@acme/db";
import { type inferAsyncReturnType } from "@trpc/server";
import type { User } from "@clerk/nextjs/api";
import { influx } from "./lib/influx";
import { RequestLike } from "@clerk/nextjs/dist/server/types";
import { getUser } from "./lib/clerk";

/**
 * This metadata comes from Clerk. We ensure that the role is typesafe if it exists.
 */
export type CustomClerkMetadata = Record<string, unknown> & {
  role?: "user" | "admin" | undefined;
};

export type UserProps = {
  user: (User & CustomClerkMetadata) | null;
};

/** Use this helper for:
 *  - testing, where we dont have to Mock Next.js' req/res
 *  - trpc's `createSSGHelpers` where we don't have req/res
 * @see https://beta.create.t3.gg/en/usage/trpc#-servertrpccontextts
 */
export const createContextInner = async ({ user }: UserProps) => {
  console.log(user);
  return {
    user,
    prisma,
    influx,
  };
};

/**
 * This is the actual context you'll use in your router
 * @link https://trpc.io/docs/context
 **/
export const createContext = async (req: RequestLike) => {
  const user = await getUser(req);
  return await createContextInner({ user });
};

export type Context = inferAsyncReturnType<typeof createContext>;

packages/api/src/lib/clerk.ts

import { RequestLike } from "@clerk/nextjs/dist/server/types";
import { getAuth, clerkClient } from "@clerk/nextjs/server";
import { inferAsyncReturnType } from "@trpc/server";
import { CustomClerkMetadata } from "../context";

export const getUser = async (req: RequestLike) => {
  const { userId } = getAuth(req);
  const user = userId ? await clerkClient.users.getUser(userId) : null;

  return user
    ? {
        ...user,
        // Clerk's privateMetadata is a `Record<string, unknown>`, so we need to parse it
        privateMetadata: user?.privateMetadata as CustomClerkMetadata,
      }
    : null;
};

export const isAdmin = (user: inferAsyncReturnType<typeof getUser>) => {
  return user?.privateMetadata?.role === "admin";
};

packages/api/src/router/auth.ts

import { isAdmin } from "../lib/clerk";
import { protectedProcedure, publicProcedure, router } from "../trpc";

export const authRouter = router({
  isAdmin: protectedProcedure.query(({ ctx }) => {
    return !!isAdmin(ctx.user);
  }),
  getSession: publicProcedure.query(({ ctx }) => {
    return ctx.user;
  }),
  getSecretMessage: protectedProcedure.query(() => {
    return "you can see this secret message!";
  }),
});

apps/web/src/middleware.ts

import { withClerkMiddleware } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { getUser, isAdmin } from "@acme/api/src/lib/clerk";

const publicPaths = ["/", "/sign-in*", "/sign-up*"];

const adminPaths = ["/admin*"];

const isPublicPath = (path: string) => {
  return publicPaths.find((x) =>
    path.match(new RegExp(`^${x}$`.replace("*$", "($|/)"))),
  );
};

const isAdminPath = (path: string) => {
  return adminPaths.find((x) =>
    path.match(new RegExp(`^${x}$`.replace("*$", "($|/)"))),
  );
};

export default withClerkMiddleware(async (req: NextRequest) => {
  if (isPublicPath(req.nextUrl.pathname)) {
    return NextResponse.next();
  }

  const user = await getUser(req);

  if (!user) {
    const signInUrl = new URL("/sign-in", req.url);
    return NextResponse.redirect(signInUrl);
  }

  if (isAdminPath(req.nextUrl.pathname) && !isAdmin(user)) {
    const signInUrl = new URL("/sign-in", req.url);
    return NextResponse.redirect(signInUrl);
  }

  return NextResponse.next();
});

export const config = {
  matcher: [
    /**
     * Match request paths except for the ones starting with:
     * - _next
     * - static (static files)
     * - favicon.ico (favicon file)
     *
     * This includes images, and requests from TRPC.
     */
    "/(.*?trpc.*?|(?!static|.*\\..*|_next|favicon.ico).*)",
  ],
};

I just updated @clerk/nextjs, and I'm now using ^4.11.2. Problem still occurs.

Sorry for the wall of text/code.

magnusrodseth commented 1 year ago

For some more context, take a look at what happens when creating the context.

packages/api/src/context.ts

/**
 * This is the actual context you'll use in your router
 * @link https://trpc.io/docs/context
 **/
export const createContext = async (req: RequestLike) => {
  console.log("checkpoint 1");
  const user = await getUser(req);
  console.log("checkpoint 2");
  return await createContextInner({ user });
};

Only checkpoint 1 gets displayed in the terminal when I refresh the page:

Screenshot 2023-03-01 at 18 12 34

This tells me that something fishy is happening in the getUser function.

packages/api/src/lib/clerk.ts

export const getUser = async (req: RequestLike) => {
  const { userId } = getAuth(req);
  const user = userId ? await clerkClient.users.getUser(userId) : null;

  console.log(user);

  return user
    ? {
        ...user,
        // Clerk's privateMetadata is a `Record<string, unknown>`, so we need to parse it
        privateMetadata: user?.privateMetadata as CustomClerkMetadata,
      }
    : null;
};

When I console.log(user) in the function, I get a sensible result with the currently logged in user. However, something clearly happens to the returned value, as checkpoint 2 is never reached.

perkinsjr commented 1 year ago

Hey thanks for this info...

I will take a look and have an answer today or tomorrow.

magnusrodseth commented 1 year ago

I can confirm that the x-clerk-auth-status is not included in the request being sent to your tRPC server. Am I supposed to set this myself, should it be set automatically, or is this a Clerk error?

Screenshot 2023-03-01 at 20 21 43
magnusrodseth commented 1 year ago

I have narrowed the problem down to originating from packages/api/src/context.ts. This is how the context looked initially, setup by Clerk:

/**
 * This is the actual context you'll use in your router
 * @link https://trpc.io/docs/context
 **/
export const createContext = async (options: CreateNextContextOptions) => {
  async function getUser() {
    const { userId } = getAuth(options.req);
    const user = userId ? await clerkClient.users.getUser(userId) : null;
    return user;
  }

  const user = await getUser();

  return await createContextInner({ user });
};

Because I wanted to pass in some slightly different data, I changed the parameter in the following way:

// Pay attention to the line below
export const createContext = async (req: RequestLike) => {
  async function getUser() {
    // Pay attention to the line below
    const { userId } = getAuth(req);
    const user = userId ? await clerkClient.users.getUser(userId) : null;
    return user;
  }

  const user = await getUser();

  return await createContextInner({ user });
};

This caused the x-clerk-auth-status to not be set in the request header.

magnusrodseth commented 1 year ago

@perkinsjr In summary, the issue was just me being stupid by tweaking the types in the tRPC context (from CreateNextContextOptions to RequestLike), thus causing the x-clerk-auth-status. I think we can close this issue with that. Do you think it's necessary to add some docs or comments about this, or do we leave it with that? ☺️

perkinsjr commented 1 year ago

Think I am going to close it in favor of not documenting this but will keep it in mind in case I need to document it in the future.