prisma: Using Prisma at the edge seems to cause React Server Components to randomly return 500s during prefetching. (probably Vercel Middleware problem with `next-auth`)

Bug description

Hello,

I’ve setup a prisma data proxy at https://cloud.prisma.io/ and I’m now using prisma at the edge in my next.js. I use the middleware and next-auth to protect my pages, routes, etc.

Problem is that, when deployed, on every page refresh a few React Server Components are supposed to be prefetched but some of them will return an error 500. Error is INTERNAL_EDGE_FUNCTION_UNHANDLED_ERROR.

If I remove the prisma adapter from next-auth it works fine.

How to reproduce

  1. Ensure to test with a Next.js app that has a few links (using the Link component) pointing to a few pages.
  2. Create and configure your Data proxy at https://cloud.prisma.io/
  3. Generate and use the prisma client at the edge
    import { PrismaClient } from "@prisma/client/edge";
    
    declare global {
      // eslint-disable-next-line no-var
      var cachedPrisma: PrismaClient;
    }
    
    let prisma: PrismaClient;
    
    if (process.env.NODE_ENV === "production") {
      prisma = new PrismaClient();
    } else {
      if (!global.cachedPrisma) {
        global.cachedPrisma = new PrismaClient();
      }
      prisma = global.cachedPrisma;
    }
    
    export const db = prisma;
    export { type PrismaClient };
    
  4. Use the next-auth prisma adapter to use the “database” session strategy for your next-auth setup.
    import NextAuth, { type DefaultSession } from 'next-auth'
    import GitHub from 'next-auth/providers/github'
    import { PrismaAdapter } from "@next-auth/prisma-adapter";
    import { db } from "./db";
    
    declare module 'next-auth' {
      interface Session {
        user: {
          /** The user's id. */
          id: string
        } & DefaultSession['user']
      }
    }
    
    export const {
      handlers: { GET, POST },
      auth,
      CSRF_experimental // will be removed in future
    } = NextAuth({
      adapter: PrismaAdapter(db),
      providers: [GitHub],
      callbacks: {
        jwt({ token, profile }) {
          if (profile) {
            token.id = profile.id
            token.image = profile.picture
          }
          return token
        },
        authorized({ auth }) {
          return !!auth?.user // this ensures there is a logged in user for -every- request
        }
      },
      pages: {
        signIn: '/sign-in' // overrides the next-auth default signin page https://authjs.dev/guides/basics/pages
      }
    })
    
  5. Ensure that your pages are “protected” via the next.js middleware.
    export { auth as middleware } from './auth'
    
    export const config = {
      matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
    }
    
  6. Go to your deployed site and observe the RSC randomly throwing 500s on each page refresh. The problem doesn’t happen during local development.
  7. Switch back to JWT session strategy by commenting out the adapter to not use it.
  8. Redeploy and see how everything is now working fine.

Expected behavior

React Server Components should be prefetched without any problems

Prisma information

// Add your schema.prisma
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["postgresqlExtensions"]
}

datasource db {
  provider          = "postgresql"
  url               = env("DATABASE_URL")
  directUrl         = env("DIRECT_DATABASE_URL")
  shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
  extensions        = [vector]
}

model Account {
  id                String   @id @default(cuid())
  createdAt         DateTime @default(now())
  updatedAt         DateTime @default(now()) @updatedAt
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?  @db.Text
  access_token      String?  @db.Text
  expires_at        Int?
  ext_expires_in    Int?
  token_type        String?
  scope             String?
  id_token          String?  @db.Text
  session_state     String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id               String          @id @default(cuid())
  createdAt        DateTime        @default(now())
  updatedAt        DateTime        @default(now()) @updatedAt
  name             String?
  email            String?         @unique
  username         String?         @unique
  emailVerified    DateTime?
  image            String?
  phone            String?
  language         String?
  country          String?
  accounts         Account[]
  sessions         Session[]
  posts            Post[]
  comments         Comment[]
  projects         ProjectUser[]
  workspaces       WorkspaceUser[]
  subscription     Subscription[]
  stripeCustomerId String?         @unique
  theme            Theme?          @relation(fields: [themeId, themeColor], references: [id, color])
  themeId          String?
  themeColor       String?
}

model VectorDocument {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt
  public    Boolean

  embedding Unsupported("vector(1536)")?
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

model Post {
  id         String    @id @default(cuid())
  createdAt  DateTime  @default(now())
  updatedAt  DateTime  @default(now()) @updatedAt
  published  Boolean   @default(false)
  title      String
  slug       String    @unique
  image      String?
  excerpt    String?   @db.Text
  content    String?   @db.Text
  comments   Comment[]
  author     User      @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId   String
  project    Project   @relation(fields: [projectId], references: [id], onDelete: Cascade)
  projectId  String
  category   Category  @relation(fields: [categoryId], references: [id])
  categoryId String
  // views      Int       @default(0)
  // likes      Int       @default(0)
  // shares     Int       @default(0)
}

model Comment {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt
  content   String   @db.Text
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  postId    String
}

model Category {
  id    String @id @default(cuid())
  slug  String @unique
  name  String
  posts Post[]
}

model ProjectUser {
  user      User    @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId    String
  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
  projectId String

  @@id([userId, projectId])
}

model Project {
  id          String        @id @default(cuid())
  createdAt   DateTime      @default(now())
  updatedAt   DateTime      @default(now()) @updatedAt
  users       ProjectUser[]
  posts       Post[]
  name        String?
  description String?       @db.Text
  slug        String?       @unique
  workspace   Workspace     @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
  workspaceId String
}

enum WorkspaceRole {
  USER
  MANAGER
  ADMIN
}

model WorkspaceUser {
  user          User          @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId        String
  workspace     Workspace     @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
  workspaceId   String
  workspaceRole WorkspaceRole @default(USER)

  @@id([userId, workspaceId])
}

model Workspace {
  id             String          @id @default(cuid())
  createdAt      DateTime        @default(now())
  updatedAt      DateTime        @default(now()) @updatedAt
  projects       Project[]
  users          WorkspaceUser[]
  name           String?
  description    String?         @db.Text
  slug           String?         @unique
  subscription   Subscription?   @relation(fields: [subscriptionId], references: [id])
  subscriptionId String?
}

model Subscription {
  id                     String      @id @default(cuid())
  createdAt              DateTime    @default(now())
  updatedAt              DateTime    @default(now()) @updatedAt
  user                   User        @relation(fields: [stripeCustomerId], references: [stripeCustomerId])
  stripeCustomerId       String
  stripeSubscriptionId   String?     @unique
  stripePriceId          String?
  stripeCurrentPeriodEnd DateTime?
  workspaces             Workspace[]
}

model Theme {
  id    String @unique
  color String
  users User[]

  @@unique([id, color])
}

Environment & setup

  • OS: macOS
  • Database: PostgreSQL
  • Node.js version: node 18 (on Vercel)

Prisma Version

prisma                  : 5.1.0
@prisma/client          : 5.1.0
Current platform        : darwin-arm64
Query Engine (Node-API) : libquery-engine a9b7003df90aa623086e4d6f4e43c72468e6339b (at node_modules/.pnpm/registry.npmjs.org+@prisma+engines@5.1.0/node_modules/@prisma/engines/libquery_engine-darwin-arm64.dylib.node)
Schema Engine           : schema-engine-cli a9b7003df90aa623086e4d6f4e43c72468e6339b (at node_modules/.pnpm/registry.npmjs.org+@prisma+engines@5.1.0/node_modules/@prisma/engines/schema-engine-darwin-arm64)
Schema Wasm             : @prisma/prisma-schema-wasm 5.1.0-28.a9b7003df90aa623086e4d6f4e43c72468e6339b
Default Engines Hash    : a9b7003df90aa623086e4d6f4e43c72468e6339b
Studio                  : 0.492.0

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 1
  • Comments: 22 (11 by maintainers)

Most upvoted comments

Sorry, let me clarify:

We are not using next-auth at all. we are using auth0 and their nextjs SDK.

We have a middleware.ts edge function that reads the auth0 session data and compares that with data in prisma, fetched via Data Proxy.

the middleware matcher is as follows:

export const config = {
  // matches all non-public and non-API routes
  // API routes have their own auth
  matcher: [
    "/",
    "/communication/:path*",
    "/consumer/:path*",
    "/crm/:path*",
    "/documents/:path*",
    "/event/:path*",
    "/eventInstance/:path*",
    "/experimental/:path*",
    "/kiosk/:path*",
    "/memberships/:path*",
    "/membershipTypes/:path*",
    "/orgGroup/:path*",
    "/payments/:path*",
    "/people/:path*",
    "/products/:path*",
    "/programs/:path*",
    "/proposedClasses/:path*",
    "/reports/:path*",
    "/settings/:path*",
    "/templateDocuments/:path*",
  ],
};

my understanding is that this wouldn’t match the __next routes that power prefetching but I’m pretty ignorant of how server components work, to be honest.

The result is, occasionally, a burst of prefetch requests that hang and then fail with 500s, with error codes that indicate an edge function failed - AFAIK the only edge function we use is the middleware.

Thanks. Looks to me this increased the probability to run into the 500.

Final question for today: What is your Data Proxy user- and project name? We’ll want to look up what is going on in the communication with your database on our side. (Can’t really be any of the Prisma side timeouts, those are not 25s by default…)

Data Proxy user: gablabelle Data Proxy project: prisma-edge-errors

FYI, I’m using a Neon serverless database. That’s why I use the shadowDatabaseUrl.

Good news: You can also remove it for Neon - they also work without shadowDatabaseUrl now:

Thanks for the heads up!

A few questions:

  • Did you configure a connection_limit when entering your Neon database connection string in Data Proxy?
  • Did you use the pooled or the unpooled connection string? If you used the pooled one, did it include the pgbouncer=true bit?

When specifying the connection string in Data Proxy, I have removed the -pooler suffix and removed the pgbouncer=true query param from the URL but I have kept the connect_timeout=10 in there.

  • Can you maybe add a few more sub pages to your reproduction, so it preloads more URLs on the main page and see if that makes the problem more prominent?

Done, deployed.

@janpio I’m a bit reassured that you were able to reproduce the issue lol. When testing again a few minutes ago, I see that the error seems to be less frequent, but it still happens though.

FYI, I’m using a Neon serverless database. That’s why I use the shadowDatabaseUrl.

Thanks for your quick responses, I’ll be waiting for your update on this and I’ll be using the JWT session strategy in the meantime.

Hello @janpio,

I have setup the requested minimal reproduction project and it’s available on Github here: https://github.com/gablabelle/prisma-edge-errors

I have also deployed that app to https://prisma-edge-errors.vercel.app/. After logging in you’ll be able to observe the errors in the dev tools’ Network tab. Upon each page refresh random pages will sometimes return 500s. Disable cache before refreshing the page.

Screenshot 2023-08-06 at 10 24 09

The error I get in the Vercel logs is the following for each page prefetching that failed with a 500:

[GET] [middleware: "src/middleware"] /dashboard reason=INTERNAL_EDGE_FUNCTION_UNHANDLED_ERROR, status=500, upstream_status=500, user_error=false

Screenshot 2023-08-06 at 10 25 05