Issue with trpc fetch when trying to pass Clerk auth to context

I'm currently trying to add Clerk auth to my trpc context, but I just keep getting this error:
Error: Unexpected token '<', "<!DOCTYPE "... is not valid JSON

This is my code:

import { NextRequest } from 'next/server'
import { getAuth } from '@clerk/nextjs/server'
import { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'

export function createContext(opts: FetchCreateContextFnOptions) {
  const auth = getAuth(opts.req as NextRequest)

  return {
    headers: opts && Object.fromEntries(opts.req.headers)

export type Context = Awaited<ReturnType<typeof createContext>>

import { TRPCError, initTRPC } from '@trpc/server'
import superjson from 'superjson'
import { ZodError } from 'zod'

import { Context } from './context'

const t = initTRPC.context<Context>().create({
  transformer: superjson,
  errorFormatter(opts) {
    const { shape, error } = opts
    return {
      data: {,
          error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
            ? error.cause.flatten()
            : null

const isAuthed = t.middleware(({ next, ctx }) => {
  console.log('isAuthed', ctx.auth)

  if (!ctx.auth.userId) {
    throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' })
  return next({
    ctx: {
      auth: ctx.auth

export const router = t.router
export const publicProcedure = t.procedure
export const protectedProcedure = t.procedure.use(isAuthed)
Everything also seems to work fine when I log in and make a call to my protectedProcedure, it's only a problem when I log out.
Kinda seems like you're getting the default 404 html page from next as a response. So maybe my first guess would be that you set up the route handler incorrectly
At least everytime I encountered an error like that it was because I had set up something wrong in the route handler
did you export the methods correctly in the route handler like this?
export {
  handler as GET,
  handler as POST,
Yep I did here's my route handler

import { fetchRequestHandler } from '@trpc/server/adapters/fetch'

import { createContext } from '~/server/api/context'
import { appRouter } from '~/server/api/root'

const handler = (req: Request) =>
    endpoint: '/api/trpc',
    router: appRouter,

export { handler as GET, handler as POST }
So after a lot of messing around it seems to caused by something with the Clerk Middleware. I'm still not exactly sure what is the problem or why though.
here's a link to a repo with the same issue as well:
I implemented this the other week, this is how I added the clerk auth to the context

import { getAuth } from '@clerk/nextjs/server'
import { inferAsyncReturnType } from '@trpc/server'
import { CreateNextContextOptions } from '@trpc/server/adapters/next'

import { prisma } from '~/server/prisma'

/** Context creator for testing purposes */
const createInnerTRPCContext = ({ req }: CreateNextContextOptions) => {
  return {
    auth: getAuth(req),

 * This is the actual context you will use in your router. It will be used to process every request
 * that goes through your tRPC endpoint.
 * @see
export const createTRPCContext = (opts: CreateNextContextOptions) => {
  return createInnerTRPCContext(opts)

export type TRPCContext = inferAsyncReturnType<typeof createTRPCContext>
and for the middleware:

import { getAuth, withClerkMiddleware } from '@clerk/nextjs/server'

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// Set the paths that don't require the user to be signed in
const publicPaths = ['/sign-in*', '/sign-up*', '/landing'],
  isPublic = (path: string) =>
    publicPaths.find((x) => path.match(new RegExp(`^${x}$`.replace('*$', '($|/)')))),
  authedEndpoints = ['/api/trpc*'],
  isAuthedEndpoint = (path: string) =>
    authedEndpoints.find((x) => path.match(new RegExp(`^${x}$`.replace('*$', '($|/)'))))

export default withClerkMiddleware((request: NextRequest) => {
  if (isPublic(request.nextUrl.pathname)) {
  // if the user is not signed in redirect them to the sign in page.
  const { userId } = getAuth(request)

  // prevent trpc calls if user not authed
  if (isAuthedEndpoint(request.nextUrl.pathname) && !userId) {
    return NextResponse.json(
        message: 'UNAUTHORIZED',
      { status: 400 }

  if (!userId) {
    // redirect the users to sign in
    const signInUrl = new URL('/sign-in', request.url)
    signInUrl.searchParams.set('redirect_url', request.url)
    return NextResponse.redirect(signInUrl)


// Stop Middleware running on static files and public folder
export const config = {
  matcher: [
     * Match all request paths except for the ones starting with:
     * - _next
     * - static (static files)
     * - favicon.ico (favicon file)
     * - public folder
     * - trpc and api endpoints
in my case any TRPC calls would happen post auth, so I added the auth check in the middleware, but feel free to remove that if your case differs
I see thank you for the information! It turns out that Clerk will send everything to the sign in page including api's so I just had to add my api route for trpc to my clerk middleware public routes.
cool, glad I was of help 🙂