Next.js Discord

Discord Forum

nextjs 'use server' middleware

Answered
Jboncz posted this in #help-forum
Open in Discord
Avatar
How can I go about catching a specific response when I call a server action and middleware rejects it due to missing authentication session?

//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.
Image
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
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. 🙂
View full answer

70 Replies

Avatar
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.
Avatar
To be clear I am using a try catch when I call my 'use server' function, but catch returns no error.
Avatar
bump
Avatar
bump
Avatar
Forest bachac
send your server action
Avatar
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?

'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
Image
Which is the proper response as described here
Image
Avatar
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.
Avatar
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 wrapper
Avatar
Yeah.... so its a current limitation of MW at this point?
Seems like a ass backwards way to get what we need lol
Avatar
mw is a concept, you can stil create middleware logic without middleware.ts
Avatar
Of course, but I have to duplicate my middleware logic.
Avatar
you can still follow the dry principle by creating a reusable function
Avatar
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
Avatar
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)
})
Avatar
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.
Avatar
i dont see a reason why you can't use the snippet I gave you in the backend
Avatar
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.
Avatar
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
Avatar
Im not following then.
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
Avatar
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
Avatar
I understand that, it was more of a troubleshooting step, I also cannot access the value of 'results' in any way.
Avatar
hmm
yes
Avatar
its undefined, because the actual server action never executed.
Avatar
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
Avatar
Gotcha, funny, returning 200 gets rid of the error, but I dont get the
{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.
Avatar
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 so
thats why you'd need a wrapper function to standardize server error, middleware, etc
which basically what next-safe-action does.
Avatar
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
Avatar
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
Avatar
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.
Avatar
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...) :dviperShrug:
Avatar
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.
Avatar
sounds like a library worth making
Avatar
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. 🙂
Avatar
@Alfonsus Ardani difference in responses.
Image
What format is that in? lol Is that form data?
Avatar
looks like RSC
:KEKW:
Avatar
It is lol.
Avatar
@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
Avatar
headers() cookies() from next/headers
its not dumb question but its a simple google search question
Avatar
Which makes it dumb 😉
Avatar
fair
Avatar
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
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
Avatar
this approach is really clean
thank you for doing the research 🔥 🔥🔥
Avatar
Thanks for troubleshooting with me to get there.