nextjs 'use server' middleware
Answered
Jboncz posted this in #help-forum
JbonczOP
How can I go about catching a specific response when I call a server action and middleware rejects it due to missing authentication session?
When using api routes the returned response is readable from whatever variable your reading from but regardless of a try/catch block on the front end when its called I get the error in the console but unable to do anything about it programmatically.
//Authentication Cookie not found
if (!authenticationCookie) {
if (requestedPath.startsWith('/api/') || serverAction != null) {
const response = NextResponse.json({ error: 'Authentication Timeout' }, { status: 412 })
const redirectLocation = referringPath;
response.cookies.set({
name: process.env.RedirectCookieName,
value: redirectLocation,
path: '/',
maxAge: 360
});
return response;
}
else {
const response = NextResponse.redirect(`${process.env.SAMLURL}`)
const redirectLocation = `${requestedPath}${requestedPathParameters}`
//Set redirect cookie
response.cookies.set({
name: process.env.RedirectCookieName,
value: redirectLocation,
path: '/',
maxAge: 360
});
//Reroute to authentication url
return response;
}
}
When using api routes the returned response is readable from whatever variable your reading from but regardless of a try/catch block on the front end when its called I get the error in the console but unable to do anything about it programmatically.
Answered by Jboncz
Okay at this time I think based on https://github.com/vercel/next.js/discussions/62446 and some addition troubleshooting I think the path of least resistance is to do something like you described with a twist.
In Middleware
Then in any server actions use
which is
I still agree with the person in that thread and am going to create an issue on it. There should be a way to invalidate an in-flight server action with the existing middleware solution. Doing it this way allows for the function to still be used as a normal backend function when not called by a Server Action
https://github.com/vercel/next.js/blob/ae524fb24499fb8caf5b70eb8ce4cc96a5301565/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts#L54
Would be as 'simple' as checking for a a json payload when executing the ServerAction and automatically returning that payload to the client as the rsc payload. Sounds simple in theory. 🙂
In Middleware
if (!authenticationCookie) {
if (serverAction != null) {
const response = NextResponse.next();
response.headers.set('Middleware-Authentication', 'false')
return response;
}
}
Then in any server actions use
const auth = serverActionAuth()
if (auth) return auth;
which is
import { headers } from 'next/headers';
export function serverActionAuth() {
const authValue = headers().get('Middleware-Authentication');
if (authValue == 'false') {
return { Authentication: false };
}
else {
return;
}
}
I still agree with the person in that thread and am going to create an issue on it. There should be a way to invalidate an in-flight server action with the existing middleware solution. Doing it this way allows for the function to still be used as a normal backend function when not called by a Server Action
https://github.com/vercel/next.js/blob/ae524fb24499fb8caf5b70eb8ce4cc96a5301565/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts#L54
Would be as 'simple' as checking for a a json payload when executing the ServerAction and automatically returning that payload to the client as the rsc payload. Sounds simple in theory. 🙂
70 Replies
JbonczOP
I know typically you would want to reauth or navigate away from an internal page but there are specific requirments to allow the page to be viewed and the data to be viewed regardless of authentication status.
JbonczOP
To be clear I am using a try catch when I call my 'use server' function, but catch returns no error.
JbonczOP
bump
JbonczOP
bump
Forest bachac
send your server action
JbonczOP
I will send it even though it isnt relevant 🙂 It isnt making it to the server action because my middleware is kicking it back.
I just cant figure out how to read the response from the middleware, as it doesnt return to the client side as a reponse, just an error to the console, which I cant even try catch for some reason?
Calling of server action
'use server';
import isimLdap_config from '@/configs/isimLdap_config';
import { ldapCreateClientAndBind } from './utilities';
export async function ldapSearchIdentities(client, filter) {
if (!client || !client.connected) {
client = await ldapCreateClientAndBind();
}
if (!filter) { throw 'Filter not provided' }
let search = await ldapSearch(client, isimLdap_config.ou_identity, filter, ['uid', 'givenname', 'sn', 'mail', 'employeetype', 'title', 'bccostcenterdesc', 'erglobalid'])
if (search.length != 1) {
return search;
}
search = await ldapGetIdentityByerglobalid(client, search[0].erglobalid);
search.roles = {};
search.accounts = {};
//Getting accounts and Services for identity
const services = await ldapGetServices(client)
const accountSearch = await ldapGetAccountsByOwner(client, search.dn);
for (const account of accountSearch) {
const accountService = services[account.erservice];
const ownershipType = account.eraccountownershiptype;
if (ownershipType && ownershipType == 'Privileged') {
search.accounts[`Priv-${accountService}`] = account;
}
else {
search.accounts[accountService] = account;
}
}
//Getting roles for identity
for (const role of search.erroles) {
try {
const roleSearch = await ldapGetRoleByDN(client, role);
let roleCategory = roleSearch.bccategory
if (!roleCategory) { roleCategory = roleSearch.bcCategory }
if (!search.roles[roleCategory]) {
search.roles[roleCategory] = [];
}
search.roles[roleCategory].push(roleSearch);
}
catch (error) {
console.log(error);
}
}
//Generating access summary
if (Object.keys(search.roles).length != 0) {
search.accessSummary = await generateAccessSummary(search.roles);
}
return search;
}
Calling of server action
const searchPerson = async (directLU) => {
let filter;
if (directLU) {
filter = directLU;
}
else {
filter = generateFilterFromQuery(query);
}
if (filter == '') {
return;
}
try {
const results = await ldapSearchIdentities(null, filter)
console.log(results)
}
catch (error) {
console.log('in error block')
console.log(error)
}
//if (Array.isArray(results)) {
// setQueryResults(results);
// setQueryResultsDialogOpen(true);
//}
//else {
// console.log(results)
//}
}
I never get into 'in error block' even though it is returning an error to the console as shown here
Which is the proper response as described here
JbonczOP
I could check if the "results" object is undefined but I feel like thats a dirty way to do this, ideally I would be able to check if the return code was 412 and key in on that being authentication failure.
https://github.com/TheEdoRan/next-safe-action You can use this (or implement something like this) to make a middleware system for the server actions
Although its not using the
middleware.ts
, it is basically creating a server action wrapperJbonczOP
Yeah.... so its a current limitation of MW at this point?
Seems like a ass backwards way to get what we need lol
mw is a concept, you can stil create middleware logic without middleware.ts
JbonczOP
Of course, but I have to duplicate my middleware logic.
you can still follow the dry principle by creating a reusable function
JbonczOP
Yeah I just dont really care about the schema stuff, I need it to check and validate the auth cookie. Its an internal portal all of IS uses. So I do check authentication in middleware, its hosted on prim, and we are fine with the drawbacks of execution time.
I mean I do care... that was a really bad way of saying, why dont actions output to the client in a way thats interceptable when its redirected from nextjs mw
const action = (cb) => {
return async (args) => {
const session = await getServerSession()
if(!session) return "Not Authenticated"
return await cb(session, args)
}
}
const s_getUserPosts = action(async (session, args) => {
return db.posts.findMany(session.id)
})
JbonczOP
So I would have to wrap my actions, the joys of having server actions is I could use the same function on the front end and backend without relying on a true route handler.
i dont see a reason why you can't use the snippet I gave you in the backend
JbonczOP
Just seems like an additional layer of complexity. Even with this I would have to let any actions go through my nextjs middleware to get to this. Ii will look at the library, but I would prefer a way to read the return code value from the action I see at the post I replied to.
Theres no way that I know of the exclude server actions from the nextjs mw, with the matcher.
its not additional layer of complexity. I provided you a way without the library
Also, can you minimize your code? Its really hard to see whats wrong with it and what you are trying to say
JbonczOP
Im not following then.
Server Action:
Client call
nextjs mw
1. Client calls Server action
2. Middleware gets invoked
3. Middleware kicks back a 412
4. client receives 412 in console, but the return value for the
Results = undefined
Server Action:
export async function ldapSearchIdentities(client, filter) {
if (!client || !client.connected) {
client = await ldapCreateClientAndBind();
}
if (!filter) { throw 'Filter not provided' }
return search;
}
Client call
const searchPerson = async (directLU) => {
try {
const results = await ldapSearchIdentities(null, filter)
console.log(results)
}
catch (error) {
console.log(error)
}
nextjs mw
if (requestedPath.startsWith('/api/') || serverAction != null) {
const response = NextResponse.json({ error: 'Authentication Timeout' }, { status: 412 })
return response;
}
1. Client calls Server action
2. Middleware gets invoked
3. Middleware kicks back a 412
4. client receives 412 in console, but the return value for the
const results = await ldapSearchIdentities(null, filter);
Results = undefined
Yeah no, you can't try catch a server action xD
everything must be returned as a value regardless if its an error or not
JbonczOP
I understand that, it was more of a troubleshooting step, I also cannot access the value of 'results' in any way.
yes
JbonczOP
its undefined, because the actual server action never executed.
yeah because when you await a server action, reactjs expect a specific format that is returned by the end point
if its not the same then it will ofc throw an error
thats why your server action must always return a 200 (unless redirected via redirect() or notFound())
you get the benefit of DX at the cost of crazy abstraction
JbonczOP
Gotcha, funny, returning 200 gets rid of the error, but I dont get the
on the client side.
{error: 'auth timeout}
on the client side.
so if I wanted to send a response from the middleware and actually get the result of the response inside of 'results' I would have to look into how server actions are transported.
Yeah im not sure what reactjs needs for the response (maybe you can try to match it from a successfull request?)
coz the common way to handle server action is basically return a json object
{ error: "Error Message" }
as json sothats why you'd need a wrapper function to standardize server error, middleware, etc
which basically what next-safe-action does.
which basically what next-safe-action does.
JbonczOP
Gotcha, this gives me a solid place to look though. I understand the concept, and the necessity for the 200. I can look into this with that information
fixed my code
yeah i hope you dont get discouraged by server action's need to always send POST and always return 200 😭
been a common pattern that you can't change status code everywhere beside Route Handler
JbonczOP
Nah, not discouraged, I just want to figure out how to send a response back from mw kinda making the client think its a server action responding lol
Agreed, I dont mind that too much. Just no clear documentation on how to intercept it. I will take apart next-safe-action and see how they do it.
well its not a direct hit from ur client code to the server, dont forget that reactjs is still the man in the middle haha
just as magicall how you cannot send ArrayBuffer to the server but you can still send FormData with Files (which is basically ArrayBuffer objects...)
JbonczOP
True, I get that. Im sure I will be able to find a solution with the information we discussed.
I think nextjs mw needs some kind of helper function for interacting with server actions though I know its 'edge' still.
sounds like a library worth making
JbonczOP
I also do think that next-safe-action is a good library, just not quite what im looking for, I do appreciate the help! Once I figure it out ill post it here for the future.
Trying to push the migration to app router at work, server actions is a win, after I figure this out. 🙂
What format is that in? lol Is that form data?
looks like RSC
JbonczOP
It is lol.
JbonczOP
@Alfonsus Ardani do you know if its possible to read the headers or cookies in the server action? I would assume that gets passed?
Sorry dumb question I asked before doing simple google search
headers() cookies() from next/headers
its not dumb question but its a simple google search question
JbonczOP
Which makes it dumb 😉
fair
JbonczOP
Okay at this time I think based on https://github.com/vercel/next.js/discussions/62446 and some addition troubleshooting I think the path of least resistance is to do something like you described with a twist.
In Middleware
Then in any server actions use
which is
I still agree with the person in that thread and am going to create an issue on it. There should be a way to invalidate an in-flight server action with the existing middleware solution. Doing it this way allows for the function to still be used as a normal backend function when not called by a Server Action
https://github.com/vercel/next.js/blob/ae524fb24499fb8caf5b70eb8ce4cc96a5301565/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts#L54
Would be as 'simple' as checking for a a json payload when executing the ServerAction and automatically returning that payload to the client as the rsc payload. Sounds simple in theory. 🙂
In Middleware
if (!authenticationCookie) {
if (serverAction != null) {
const response = NextResponse.next();
response.headers.set('Middleware-Authentication', 'false')
return response;
}
}
Then in any server actions use
const auth = serverActionAuth()
if (auth) return auth;
which is
import { headers } from 'next/headers';
export function serverActionAuth() {
const authValue = headers().get('Middleware-Authentication');
if (authValue == 'false') {
return { Authentication: false };
}
else {
return;
}
}
I still agree with the person in that thread and am going to create an issue on it. There should be a way to invalidate an in-flight server action with the existing middleware solution. Doing it this way allows for the function to still be used as a normal backend function when not called by a Server Action
https://github.com/vercel/next.js/blob/ae524fb24499fb8caf5b70eb8ce4cc96a5301565/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts#L54
Would be as 'simple' as checking for a a json payload when executing the ServerAction and automatically returning that payload to the client as the rsc payload. Sounds simple in theory. 🙂
Answer
this approach is really clean
thank you for doing the research 🔥 🔥🔥
JbonczOP
Thanks for troubleshooting with me to get there.