'req' of undefined in onError of express middleware

MMugetsu4/3/2023
I've recently noticed that I get a bunch of errors regarding req object missing in ctx for onError property in trpc express middleware. Can't figure out how req obj might be undefined at the point of onerror handler??

 app.use(
    '/trpc',
    trpcExpress.createExpressMiddleware({
      router: appRouter,
      createContext: createTRPCContext,
      onError: ({ path: errorPath, error, ctx }) => {
        if (isDevelopment) {
          ctx.req.logger.debug(
            `❌❌❌ tRPC failed on ${errorPath ?? '<no-path>'}: ${
              error.message
            }`,
          )
        }

        if (error.code === 'INTERNAL_SERVER_ERROR') ctx.req.logger.error(error)
        else ctx.req.logger.warn(error)
      },
    }),
  )


Unhandled rejection, reason: TypeError: Cannot read property 'req' of undefined     at Object.onError (/home/app/server/index.js:135:90)     at onError (/home/app/node_modules/.pnpm/@trpc+server@10.17.0/node_modules/@trpc/server/dist/nodeHTTPRequestHandler-11f3df04.js:72:32)     at Object.resolveHTTPResponse (/home/app/node_modules/.pnpm/@trpc+server@10.17.0/node_modules/@trpc/server/dist/resolveHTTPResponse-94b380d2.js:187:18)     at /home/app/node_modules/.pnpm/@trpc+server@10.17.0/node_modules/@trpc/server/dist/nodeHTTPRequestHandler-11f3df04.js:63:50     at async /home/app/node_modules/.pnpm/@trpc+server@10.17.0/node_modules/@trpc/server/dist/adapters/express.js:16:9
}
Nnlucas4/3/2023
It’s actually ctx which is undefined
Nnlucas4/3/2023
Can you share your createContext? Also do you have any middlewares doing context swapping?
MMugetsu4/3/2023
Ahh so seems I setup something in a wrong way coz I was sure ctx will have req/res at all times.
MMugetsu4/3/2023
yeah sure here is my whole setup
MMugetsu4/3/2023
import { TRPCError, initTRPC } from '@trpc/server'
import superjson from 'superjson'
import * as trpcExpress from '@trpc/server/adapters/express'
import express from 'express'

import { cleanAuthorisation } from '../middleware/auth-middleware'
import {
  SchemaType,
  UserRoles,
} from '../../universal/components/utils/constants'

type CreateContextOptions = {
  req: express.Request
  res: express.Response
  currentSchema: any
}

type MetaOptions = {
  allowedRoles: UserRoles[]
}

export const createInnerTRPCContext = async (opts: CreateContextOptions) => ({
  req: opts.req,
  res: opts.res,
  currentSchema: opts.currentSchema,
})

const getCurrentSchema = (req: express.Request) => {
  const { schemaId, locationSchemas } = req

  return (
    locationSchemas.find(({ id }) => id === schemaId) ||
    locationSchemas.find(({ canWrite }) => canWrite) ||
    locationSchemas[0]
  )
}

export const createTRPCContext = async (
  opts: trpcExpress.CreateExpressContextOptions,
) => {
  const { req, res } = opts
  const currentSchema = getCurrentSchema(req)

  return createInnerTRPCContext({
    req,
    res,
    currentSchema,
  })
}
MMugetsu4/3/2023
const t = initTRPC
  .context<typeof createTRPCContext>()
  .meta<MetaOptions>()
  .create({
    transformer: superjson,
    errorFormatter({ shape, ctx }) {
      const { data, ...rest } = shape

      return {
        ...rest,
        data: {
          ...data,
          traceId: ctx?.req?.traceId,
          schemaId: ctx?.req?.schemaId,
        },
      }
    },
    defaultMeta: { allowedRoles: [] },
  })

export const createTRPCRouter = t.router

const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.req?.authorisation?.authorised) {
    cleanAuthorisation(ctx.req)

    throw new TRPCError({
      code: 'UNAUTHORIZED',
      message: 'User not authorised',
    })
  }

  return next({
    ctx: {
      req: ctx.req,
      res: ctx.res,
    },
  })
})

const enforceUserRoles = enforceUserIsAuthed.unstable_pipe(
  ({ ctx, meta, next }) => {
    const currentRoles = ctx.req?.authorisation?.userSecurityGroups ?? []
    const allowedRoles = meta?.allowedRoles ?? []

    if (!currentRoles.length) {
      throw new TRPCError({
        code: 'INTERNAL_SERVER_ERROR',
        message: 'User roles are missing',
      })
    }

    if (
      allowedRoles.length &&
      !allowedRoles.some((role) => currentRoles.includes(role))
    ) {
      throw new TRPCError({
        code: 'FORBIDDEN',
        message: 'User role not allowed',
      })
    }

    return next({ ctx })
  },
)

const enforceValidSchema = enforceUserRoles.unstable_pipe(({ ctx, next }) => {
  const { schemaId } = ctx.req

  if (schemaId && !Object.values(SchemaType).includes(schemaId)) {
    throw new TRPCError({
      code: 'INTERNAL_SERVER_ERROR',
      message: `SchemaId not supported: (${schemaId})`,
    })
  }

  return next({ ctx })
})

export const publicProcedure = t.procedure
export const procedure = t.procedure.use(enforceValidSchema)
Nnlucas4/3/2023
context<typeof createTRPCContext>() needs a inferAsyncReturnType<> sprinkled in there, but might not be your issue here
Nnlucas4/3/2023
Honestly this does look okay, it's a nice clean setup
Nnlucas4/3/2023
enforceUserIsAuthed also shouldn't need to add the req/res to context since you already did that
Nnlucas4/3/2023
I'm really just finding silly things though, and am confused why you're getting an undefined context
Nnlucas4/3/2023
If you're able to throw together a reproduction in stackblitz or something, a GitHub issue might be in order
MMugetsu4/3/2023
Yeah it will be hard to reproduce. No matter how hard I try I can't reproduce it locally. Happening only on Prod. I will try add a bit more logging maybe that will help me track down the problem.
Nnlucas4/3/2023
That might be a more useful clue than you think
Nnlucas4/3/2023
Figure out what's really different in prod, in many cases it's quite a lot!