Mugetsu
Mugetsu17mo ago

'req' of undefined in onError of express middleware

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)
},
}),
)
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
}
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
}
5 Replies
Nick
Nick17mo ago
It’s actually ctx which is undefined Can you share your createContext? Also do you have any middlewares doing context swapping?
Mugetsu
Mugetsu17mo ago
Ahh so seems I setup something in a wrong way coz I was sure ctx will have req/res at all times. yeah sure here is my whole setup
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,
})
}
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,
})
}
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)
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)
Nick
Nick17mo ago
context<typeof createTRPCContext>() needs a inferAsyncReturnType<> sprinkled in there, but might not be your issue here Honestly this does look okay, it's a nice clean setup enforceUserIsAuthed also shouldn't need to add the req/res to context since you already did that I'm really just finding silly things though, and am confused why you're getting an undefined context If you're able to throw together a reproduction in stackblitz or something, a GitHub issue might be in order
Mugetsu
Mugetsu17mo ago
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.
Nick
Nick17mo ago
That might be a more useful clue than you think Figure out what's really different in prod, in many cases it's quite a lot!