Tom
Tomβ€’10mo ago

Return TRPC Error from NextJS middleware

I am using trpc for my app's api but Im using NextJS middleware + upstash ratelimitting for.... well ratelimitting. Is there a way return a TRPC error from the middleware route?
26 Replies
Tom
Tomβ€’10mo ago
Bump?
Alex / KATT 🐱
Alex / KATT πŸ±β€’10mo ago
Throw it and use an error formatter
Tom
Tomβ€’10mo ago
well in the nextjs middleware i can't really throw it. i could superjson the error and res.send() it. would that work?
BillyBob
BillyBobβ€’5mo ago
@Tom How did you get on with this? and how were you able to run tRPC in the middleware? I am currently trying to redirect if a user is not authenticated in nextjs middleware Something like this:
import { api } from './app/trpc/server'
import { TRPCClientError } from '@trpc/client'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
console.log('Haha LOL 123')
return api.userRouter.getCurrentUser.query().catch((err) => {
if (err instanceof TRPCClientError) {
if (err.data.code === 'UNAUTHORIZED') {
return NextResponse.redirect(new URL('/login', request.url))
}
}
})
}

//See "Matching Paths" below to learn more
export const config = {
matcher: '/dashboard/',
}
import { api } from './app/trpc/server'
import { TRPCClientError } from '@trpc/client'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
console.log('Haha LOL 123')
return api.userRouter.getCurrentUser.query().catch((err) => {
if (err instanceof TRPCClientError) {
if (err.data.code === 'UNAUTHORIZED') {
return NextResponse.redirect(new URL('/login', request.url))
}
}
})
}

//See "Matching Paths" below to learn more
export const config = {
matcher: '/dashboard/',
}
but i keep getting the error : Invariant: headers() expects to have requestAsyncStorage, none available.
Tom
Tomβ€’5mo ago
To be honest, I never got a great answer. So I just manually worked around it on the client side. So if I ever got a json error from my service I assume it was the middleware rejecting it
BillyBob
BillyBobβ€’5mo ago
I got advised today to do a simple fetch() query to the tRPC endpoint. The code snippet above i shared did not work at all
MarvinKR
MarvinKRβ€’5mo ago
Why use NextJS middleware and not t.middleware?
BillyBob
BillyBobβ€’5mo ago
@MarvinKR Because in my case I want to redirect the user when they become unauthenticated
MarvinKR
MarvinKRβ€’5mo ago
Can’t you redirect inside tRPC?
BillyBob
BillyBobβ€’5mo ago
Apparently not unless I am missing something? Isn’t trpc doing fetch requests under the hood? So a redirect would not work.
Alex / KATT 🐱
Alex / KATT πŸ±β€’5mo ago
you're correct this looks good to me
BillyBob
BillyBobβ€’5mo ago
I couldn't get the above to work.
Alex / KATT 🐱
Alex / KATT πŸ±β€’5mo ago
seems to be something with client setup
BillyBob
BillyBobβ€’5mo ago
I am using App Router with NextJS 14 - Monorepo but with separated server and client This works: But i don't like that it will do an extra API call on every request.
const fetchUser = (req: NextRequest): Promise<User | null> =>
fetch('http://localhost:4000/trpc/userRouter.getCurrentUser', {
headers: Object.fromEntries(req.headers),
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`)
}
return response.json() as Promise<ApiResponse>
})
.then(({ result }) => result.data.json)
.catch((error) => {
console.error('Error fetching user:', error)
return null // Return null or handle the error in an appropriate way
})

export async function middleware(request: NextRequest) {
const user = await fetchUser(request)
if (!user) return NextResponse.redirect(new URL('/login', request.url))
return NextResponse.next()
}
const fetchUser = (req: NextRequest): Promise<User | null> =>
fetch('http://localhost:4000/trpc/userRouter.getCurrentUser', {
headers: Object.fromEntries(req.headers),
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`)
}
return response.json() as Promise<ApiResponse>
})
.then(({ result }) => result.data.json)
.catch((error) => {
console.error('Error fetching user:', error)
return null // Return null or handle the error in an appropriate way
})

export async function middleware(request: NextRequest) {
const user = await fetchUser(request)
if (!user) return NextResponse.redirect(new URL('/login', request.url))
return NextResponse.next()
}
Alex / KATT 🐱
Alex / KATT πŸ±β€’5mo ago
can you show me your experimental_createTRPCNextAppDirServer-setup? i have an idea or like show me where const api is defined
BillyBob
BillyBobβ€’5mo ago
'use server'

import { headers } from 'next/headers'
import type { TRPCLink } from '@trpc/client'
import { loggerLink, httpBatchLink, createTRPCProxyClient } from '@trpc/client'
import superjson from 'superjson'
import type { AppRouter } from '@api/server/router'
import { observable } from '@trpc/server/observable'
import { permanentRedirect } from 'next/navigation'

// export const api = experimental_createTRPCNextAppDirServer<AppRouter>({
// config() {
// return {
// transformer: superjson,
// links: [
// loggerLink({
// enabled: (opts) => {
// //console.log(opts)
// return (
// process.env.NODE_ENV === 'development' ||
// (opts.direction === 'down' && opts.result instanceof Error)
// )
// },
// }),
// httpBatchLink({
// url: 'http://localhost:4000/trpc',
// headers: Object.fromEntries(headers()),
// fetch(url, options) {
// return fetch(url, {
// ...options,
// credentials: 'include',
// })
// },
// }),
// ],
// }
// },
// })

export const api = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
// loggerLink commented out
//customLink,
httpBatchLink({
url: 'http://localhost:4000/trpc',
headers: Object.fromEntries(headers()),
fetch(url, options) {
return fetch(url, {
...options,
credentials: 'include',
})
},
}),
],
})
'use server'

import { headers } from 'next/headers'
import type { TRPCLink } from '@trpc/client'
import { loggerLink, httpBatchLink, createTRPCProxyClient } from '@trpc/client'
import superjson from 'superjson'
import type { AppRouter } from '@api/server/router'
import { observable } from '@trpc/server/observable'
import { permanentRedirect } from 'next/navigation'

// export const api = experimental_createTRPCNextAppDirServer<AppRouter>({
// config() {
// return {
// transformer: superjson,
// links: [
// loggerLink({
// enabled: (opts) => {
// //console.log(opts)
// return (
// process.env.NODE_ENV === 'development' ||
// (opts.direction === 'down' && opts.result instanceof Error)
// )
// },
// }),
// httpBatchLink({
// url: 'http://localhost:4000/trpc',
// headers: Object.fromEntries(headers()),
// fetch(url, options) {
// return fetch(url, {
// ...options,
// credentials: 'include',
// })
// },
// }),
// ],
// }
// },
// })

export const api = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
// loggerLink commented out
//customLink,
httpBatchLink({
url: 'http://localhost:4000/trpc',
headers: Object.fromEntries(headers()),
fetch(url, options) {
return fetch(url, {
...options,
credentials: 'include',
})
},
}),
],
})
Ive been switching between createTRPCProxyClient and experimental_createTRPCNextAppDirServer see above
customLink: Currently not working
export const customLink: TRPCLink<AppRouter> = () => {
return ({ next, op }) => {
return observable((observer) => {
const unsubscribe = next(op).subscribe({
next(value) {
observer.next(value)
},
error(err) {
observer.error(err)
if (err?.data?.code === 'UNAUTHORIZED') {
console.log('LIAM123') // Works
permanentRedirect('/login') // Does not work both 'redirect' and 'permanentRedirect'
}
},
complete() {
observer.complete()
},
})
return unsubscribe
})
}
}
customLink: Currently not working
export const customLink: TRPCLink<AppRouter> = () => {
return ({ next, op }) => {
return observable((observer) => {
const unsubscribe = next(op).subscribe({
next(value) {
observer.next(value)
},
error(err) {
observer.error(err)
if (err?.data?.code === 'UNAUTHORIZED') {
console.log('LIAM123') // Works
permanentRedirect('/login') // Does not work both 'redirect' and 'permanentRedirect'
}
},
complete() {
observer.complete()
},
})
return unsubscribe
})
}
}
Alex / KATT 🐱
Alex / KATT πŸ±β€’5mo ago
so something like this maybe
export const api = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
// loggerLink commented out
//customLink,
httpBatchLink({
url: 'http://localhost:4000/trpc',
headers(opts) {
// Any of the passed operations can override the headers
const headersOverride = opts.opList.find(
(op) => op.context?.headersOverride,
) as Record<string, string> | undefined;

if (headersOverride) {
console.log('headersOverride', headersOverride);
return headersOverride;
}

return Object.fromEntries(headers());
},
fetch(url, options) {
return fetch(url, {
...options,
credentials: 'include',
});
},
}),
],
});
export const api = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
// loggerLink commented out
//customLink,
httpBatchLink({
url: 'http://localhost:4000/trpc',
headers(opts) {
// Any of the passed operations can override the headers
const headersOverride = opts.opList.find(
(op) => op.context?.headersOverride,
) as Record<string, string> | undefined;

if (headersOverride) {
console.log('headersOverride', headersOverride);
return headersOverride;
}

return Object.fromEntries(headers());
},
fetch(url, options) {
return fetch(url, {
...options,
credentials: 'include',
});
},
}),
],
});
& this becomes something like
import { TRPCClientError } from '@trpc/client';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { api } from './app/trpc/server';

// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
console.log('Haha LOL 123');
return api.userRouter.getCurrentUser
.query(undefined, {
context: {
headersOverride: Object.entries(request.headers),
},
})
.catch((err) => {
if (err instanceof TRPCClientError) {
if (err.data.code === 'UNAUTHORIZED') {
return NextResponse.redirect(new URL('/login', request.url));
}
}
});
}

//See "Matching Paths" below to learn more
export const config = {
matcher: '/dashboard/',
};
import { TRPCClientError } from '@trpc/client';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { api } from './app/trpc/server';

// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
console.log('Haha LOL 123');
return api.userRouter.getCurrentUser
.query(undefined, {
context: {
headersOverride: Object.entries(request.headers),
},
})
.catch((err) => {
if (err instanceof TRPCClientError) {
if (err.data.code === 'UNAUTHORIZED') {
return NextResponse.redirect(new URL('/login', request.url));
}
}
});
}

//See "Matching Paths" below to learn more
export const config = {
matcher: '/dashboard/',
};
BillyBob
BillyBobβ€’5mo ago
I can test this.
Alex / KATT 🐱
Alex / KATT πŸ±β€’5mo ago
basically override on demand for middlewares
BillyBob
BillyBobβ€’5mo ago
Thank you very much for helping
Alex / KATT 🐱
Alex / KATT πŸ±β€’5mo ago
the underlying issue seems to be that you can't call headers() in middlewares
BillyBob
BillyBobβ€’5mo ago
Am i likely to be able to get the customLink to work? That way i wouldn't be doing an extra API call on every request
Alex / KATT 🐱
Alex / KATT πŸ±β€’5mo ago
perhaps, i'm not sure
BillyBob
BillyBobβ€’5mo ago
@Alex / KATT 🐱 Edit: Your solution works. At first it was not entering the catch, but that was my fault. btw your solution did solve another issue for me though: I was intermittently getting Error: Invariant: headers() expects to have requestAsyncStorage, in some route page.tsx @Alex / KATT 🐱 - Actually had to make some changes but it worked in the end:
export const api = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
// loggerLink({
//customLink,
httpBatchLink({
url: 'http://localhost:4000/trpc',
headers(opts) {
// Any of the passed operations can override the headers
const headersOverride = opts.opList.find(
(op) => op.context?.headersOverride,
)?.context.headersOverride as Record<string, string> | undefined // THIS LINE WAS UPDATED

if (headersOverride) {
console.log('headersOverride', headersOverride)
//console.log('headers', Object.fromEntries(headers()))
return headersOverride
}

return Object.fromEntries(headers())
},
fetch(url, options) {
return fetch(url, {
...options,
credentials: 'include',
})
},
}),
],
})
export const api = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
// loggerLink({
//customLink,
httpBatchLink({
url: 'http://localhost:4000/trpc',
headers(opts) {
// Any of the passed operations can override the headers
const headersOverride = opts.opList.find(
(op) => op.context?.headersOverride,
)?.context.headersOverride as Record<string, string> | undefined // THIS LINE WAS UPDATED

if (headersOverride) {
console.log('headersOverride', headersOverride)
//console.log('headers', Object.fromEntries(headers()))
return headersOverride
}

return Object.fromEntries(headers())
},
fetch(url, options) {
return fetch(url, {
...options,
credentials: 'include',
})
},
}),
],
})
export async function middleware(request: NextRequest) {
return api.userRouter.getCurrentUser
.query(undefined, {
context: {
headersOverride: Object.fromEntries(request.headers), // entries() changed to fromEntries()
},
})
.then(() => NextResponse.next())
.catch((err) => {
if (err instanceof TRPCClientError) {
if (err.data.code === 'UNAUTHORIZED') {
console.log('in if')
return NextResponse.redirect(new URL('/login', request.url))
}
}
return NextResponse.next()
})
}
export async function middleware(request: NextRequest) {
return api.userRouter.getCurrentUser
.query(undefined, {
context: {
headersOverride: Object.fromEntries(request.headers), // entries() changed to fromEntries()
},
})
.then(() => NextResponse.next())
.catch((err) => {
if (err instanceof TRPCClientError) {
if (err.data.code === 'UNAUTHORIZED') {
console.log('in if')
return NextResponse.redirect(new URL('/login', request.url))
}
}
return NextResponse.next()
})
}
Alex / KATT 🐱
Alex / KATT πŸ±β€’5mo ago
nice, glad it worked out!
BillyBob
BillyBobβ€’4mo ago
@Alex / KATT 🐱 I think this might be interfereing with POST requests / mutations as there is a content-length mismatch. If you have a quick fix by better understanding that would be helpful. I am working on it now though Yes i am having to do this to get it to work
headers(opts) {
// Any of the passed operations can override the headers
const headersOverride = opts.opList.find(
(op) => op.context?.headersOverride
)?.context.headersOverride as Record<string, string> | undefined



if (headersOverride) {
// console.log('headersOverride', headersOverride)
// console.log('headers', Object.fromEntries(headers()))
delete headersOverride['content-length']
delete headersOverride['content-type']
return headersOverride
}

const h = Object.fromEntries(headers())
delete h['content-length']
delete h['content-type']

return h
}
headers(opts) {
// Any of the passed operations can override the headers
const headersOverride = opts.opList.find(
(op) => op.context?.headersOverride
)?.context.headersOverride as Record<string, string> | undefined



if (headersOverride) {
// console.log('headersOverride', headersOverride)
// console.log('headers', Object.fromEntries(headers()))
delete headersOverride['content-length']
delete headersOverride['content-type']
return headersOverride
}

const h = Object.fromEntries(headers())
delete h['content-length']
delete h['content-type']

return h
}
seems messy also not sure what changed. but now a simple
headers() {
return {
cookie: cookies().toString(),
}
},
headers() {
return {
cookie: cookies().toString(),
}
},
seems to work