Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error: NEXT_REDIRECT while using server actions #49298

Closed
1 task done
ghoshnirmalya opened this issue May 5, 2023 · 29 comments
Closed
1 task done

Error: NEXT_REDIRECT while using server actions #49298

ghoshnirmalya opened this issue May 5, 2023 · 29 comments
Labels
area: app App directory (appDir: true) bug Issue was opened via the bug report template. locked

Comments

@ghoshnirmalya
Copy link

ghoshnirmalya commented May 5, 2023

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 22.4.0: Mon Mar  6 21:01:02 PST 2023; root:xnu-8796.101.5~3/RELEASE_ARM64_T8112
Binaries:
  Node: 19.2.0
  npm: 9.6.2
  Yarn: 1.22.19
  pnpm: 8.4.0
Relevant packages:
  next: 13.4.1-canary.2
  eslint-config-next: 13.4.0
  react: 18.2.0
  react-dom: 18.2.0

Which area(s) of Next.js are affected? (leave empty if unsure)

App directory (appDir: true)

Link to the code that reproduces this issue

https://github.com/ghoshnirmalya/the-fullstack-app/blob/next-server-actions/src/components/admin/Forum/ForumCreateForm.tsx#L20-L68

To Reproduce

If the server actions don't use a try...catch block, then there is no error. The form action works as expected. However, if there is a try...catch block, then the error Error: NEXT_REDIRECT shows up.

Note that I'm using Prisma and the data gets saved before calling redirect(/admin/forums/${forum.id});. The forum object has the correct data.

Describe the Bug

The following code works as expected:

const handleSubmit = async (formData: FormData) => {
  "use server";

  const session = await getServerSession(authOptions);

  if (!session) {
    throw new Error("Unauthorized");
  }

  const forum = await prisma.forum.create({
    data: {
      title: String(formData.get("title")),
      description: String(formData.get("description")),
      creatorId: session.user.id,
    },
  });

  redirect(`/admin/forums//${forum.id}`);
}

However, if I use a try...catch block, then the following error showing up:
Screenshot 2023-05-05 at 5 36 20 PM

The code that throws the above error is as follows:

const handleSubmit = async (formData: FormData) => {
  "use server";

  try {
    const session = await getServerSession(authOptions);

    if (!session) {
      throw new Error("Unauthorized");
    }

    const data = Object.fromEntries(formData.entries());

    const { title, description } = forumCommentCreateSchema.parse(data);

    const forum = await prisma.forum.create({
      data: {
        title,
        description,
        creatorId: session.user.id,
      },
    });

    redirect(`/admin/forums/${forum.id}`);
  } catch (error) {
    if (error instanceof z.ZodError) {
      throw new Error(JSON.stringify(error.issues));
    }

    throw new Error("Something went wrong.");
  }
};

Expected Behavior

The redirect() function should redirect to the correct URL instead of throwing an error.

Which browser are you using? (if relevant)

Chromium Engine Version 112.0.5615.137

How are you deploying your application? (if relevant)

Vercel

@ghoshnirmalya ghoshnirmalya added the bug Issue was opened via the bug report template. label May 5, 2023
@github-actions github-actions bot added the area: app App directory (appDir: true) label May 5, 2023
@erxonxi
Copy link

erxonxi commented May 5, 2023

He have the same bug!

This is my code if is useful:

"use server"

import { CourseId } from "@/Contexts/Mooc/Shared/domain/Courses/CourseId";
import { MongoClientFactory } from "@/Contexts/Shared/infrastructure/persistence/mongo/MongoClientFactory";
import { MongoCourseRepository } from "@/Contexts/Mooc/Courses/infrastructure/persistence/MongoCourseRepository";
import { Course } from "@/Contexts/Mooc/Courses/domain/Course";
import { redirect } from "next/navigation";
import { RedirectType } from "next/dist/client/components/redirect";

export async function createCourse(form: FormData) {
	try {
		const data = {
			id: CourseId.random().value,
			name:  form.get("name") as string,
			duration: form.get("duration") as string,
		}

		const url = "mongodb://localhost:27017/next13-fullstack"
		const client = MongoClientFactory.createClient("mooc", { url })
		const repository = new MongoCourseRepository(client)

		const course = Course.fromPrimitives(data)
		await repository.save(course);

		redirect("/", RedirectType.push)
	} catch (error) {
		let message= "Something went wrong"

		console.log(error)

		if (error instanceof Error) {
			message = error.message
		}


		redirect(`/?error=${message}`, RedirectType.push)
	}
}
import styles from "./CourseForm.module.css";

import { createCourse } from "@/components/CourseForm/actions/createCourse";

export function CourseForm() {

	return (
		<form className={styles.form}>

			<div className={styles.form__field}>
				<label className={styles.form__label} htmlFor="name">Name</label>
				<input className={styles.form__input} type="text" name="name" id="name" />
			</div>

			<div className={styles.form__field}>
				<label className={styles.form__label} htmlFor="duration">Duration</label>
				<input className={styles.form__input} type="text" name="duration" id="duration" />
			</div>

			<button className={styles.form__button} formAction={createCourse}>Create new course</button>
		</form>
	)
}

@JoseVSeb
Copy link

JoseVSeb commented May 7, 2023

I'm not using form action, but manually triggering server action using client-side javascript. I have the same issue. redirect is not working.
call stack, if helpful:
image

P.S. I don't know why but I can't find how to enable the call stack logging in the server console, and so the web capture. please comment if you know how to enable the log.

@vasco3
Copy link

vasco3 commented May 7, 2023

I removed the try-catch from my code and now the redirect works, thanks

@JoseVSeb
Copy link

JoseVSeb commented May 7, 2023

I removed the try-catch from my code and now the redirect works, thanks

Mine doesn't have any try catch. I don't do error handling until the main flow of a feature is complete.

@jeengbe
Copy link

jeengbe commented May 7, 2023

You have to place the redirect() outside try-catch. Internally, redirect() is throwing an Error object (see https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/redirect.ts#L12-L36), which is also why it's typed as function redirect(...): never.

Try to place less code in a try-catch anyway. You'll get the same behaviour of throw new Error("Unauthorized");.

If you have a global error handler/wrapper, you can rethrow it with something like:

} catch (err) {
  if(isRedirectError(err)) throw err;

  // ...
}

This sounds like something an eslint rule should be able to handle well

@ghoshnirmalya
Copy link
Author

@jeengbe I'm using it inside a server action:

const handleEdit = async (formData: FormData) => {
 const handleEdit = async (formData: FormData) => {
  "use server";

  try {
    await update({
      id: Number(forum.id),
      title: String(formData.get("title")),
      content: String(formData.get("content")),
    });

    revalidatePath(`/admin/forums/${forum.id}`);
    redirect(`/admin/forums`);
  } catch (error) {
    console.log(error);
  }
};

As you can see above, I would like to revalidate a path and redirect to the forums list page only if the update request is successful.

Can you please suggest a way in which I can do this without a try...catch block?

@jeengbe
Copy link

jeengbe commented May 7, 2023

You can limit the scope of your try-catch:

const handleEdit = async (formData: FormData) => {
  "use server";

  try {
    await update({
      id: Number(forum.id),
      title: String(formData.get("title")),
      content: String(formData.get("content")),
    });
  } catch (error) {
    console.log(error);
  }

  revalidatePath(`/admin/forums/${forum.id}`);
  redirect(`/admin/forums`);
};

@ghoshnirmalya
Copy link
Author

Won't the above handleEdit function redirect to /admin/forms even if the update function returns an error?

@JoseVSeb
Copy link

JoseVSeb commented May 7, 2023

this is my server-side code:

class AdmissionService {
  @Validate
  async initiate(@YupSchema(StudentSchema) data: StudentSchema) {
    const userId = "8b896311-e958-4ece-872c-aac11a497dea";
    const admission = await prisma.workflow.create({
      data: {
        data,
        type: "ADMISSION",
        requestedById: userId,
        status: "PENDING",
      },
      select: { id: true },
    });
    redirect(`/workflows/admission/${admission.id}`);
  }
}

I have confirmed that the x-action-redirect header is in the response with the correct redirect path /workflows/admission/d65a2be3-bf7d-4514-8606-8815295767a8.
redirection fails in client side.
image

image
image
image

@timneutkens
Copy link
Member

As others have pointed out the way redirect() and notFound() work is by throwing an error that Next.js can handle in order to stop execution of the code.

 const handleEdit = async (formData: FormData) => {
  "use server";

  try {
    await update({
      id: Number(forum.id),
      title: String(formData.get("title")),
      content: String(formData.get("content")),
    });

    revalidatePath(`/admin/forums/${forum.id}`);
    redirect(`/admin/forums`);
  } catch (error) {
    // you end up catching the `redirect()` above in this case.
    console.log(error);
  }
};

One way to fix it:

 const handleEdit = async (formData: FormData) => {
  "use server";

  try {
    await update({
      id: Number(forum.id),
      title: String(formData.get("title")),
      content: String(formData.get("content")),
    });
  }  catch (error) {
    console.log(error);
    // Re-throw the error, that way `revalidatePath` / `redirect` are not executed.
    throw error
  }

  revalidatePath(`/admin/forums/${forum.id}`);
  redirect(`/admin/forums`);
};

We'll make sure the docs are updated to explain redirect() and notFound() use error throwing under the hood.

@duongductrong
Copy link

You can use transition hook to wrapped the server action, it looks like this:
CleanShot 2023-05-12 at 10 11 21

@benjaminwaterlot
Copy link

I noticed a behaviour that's not really intuitive at first:

→ The redirect() only works if my server action call is returned in the startTransition() callback.

Otherwise said:

This works

startTransition(() => {
  return updateRecipientInfo(values);
});

This breaks with a NEXT_REDIRECT error in the console, and no redirection happening.

startTransition(() => {
  updateRecipientInfo(values);
});

I believe this is the source for your error @JoseVSeb.

Is this behaviour expected @timneutkens ?

@JoseVSeb
Copy link

@benjaminwaterlot thanks, that was the issue. Although, i don't understand why that's needed. useTransition hook merely provides a pending state for promise resolution. Is server action using the hook to somehow detect redirection?

@timneutkens
Copy link
Member

timneutkens commented May 12, 2023

Assuming updateRecipientInfo is a server action, the code provided is a dangling promise, whereas when you return it's an awaited promise.

startTransition(() => {
  const value = updateRecipientInfo(values);
  // `value` is a promise. It's not awaited/then'ed so it ends up throwing in the global scope.
});

The right way to go about it is this:

startTransition(async () => {
  await updateRecipientInfo(values);
});

In the experimental version of React that is enabled when you enable server actions async transitions are supported (it's what action uses under the hood too.

Hope that clarifies why it would not be caught when you "just call it" instead of returning. It's similar to not awaiting any other async function

@benjaminwaterlot
Copy link

Thanks a lot for your answer 🙂

I didn't realize an error in a dangling promise couldn't be caught, and by reflex I used this the same way the nextjs documentation uses router.refresh() in startTransition, although that's not the same thing at all.

Maybe an uncaught NEXT_REDIRECT would benefit from a more helpful error message? (I believe lots of newcomers will get bitten by that error).

@sshnavin
Copy link

Hey folks,

I'm encountering the same issue with the latest release 13.4.3 something as simple as this:

const Login = async (email: string, password: string) => {
    'use server';
    const res = await fetch(`https://myurl.com/login`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email,
        password,
      }),
      cache: 'no-store',
    });
    const { error, data } = await res.json();
    if (error === false) cookies().set('accessToken', data.accessToken);
    redirect('/dashboard');
  };

Gives me the same redirect error as the OP, what am i missing?

@aaronvg
Copy link

aaronvg commented May 31, 2023

Also facing the same thing -- are we not supposed to redirect inside server actions?

nvm-- fixed it by returning the promise in the onStart(() => .. ) inside the button's onClick

@codinginflow
Copy link

You can use transition hook to wrapped the server action, it looks like this: CleanShot 2023-05-12 at 10 11 21

Can you explain what the purpose of useTransition is here? I've seen it used with server actions a couple times but I don't get the point!

@nachaos
Copy link

nachaos commented Jun 19, 2023

Dealing with this now, found a discussion that relates #50170

@Lebraz98
Copy link

I removed try catch and works idk why but its works

@skyksandr
Copy link

skyksandr commented Jul 20, 2023

@JawadFadel because redirect and notFound are throwing errors. And you need to either remove try/catch or handle and re-throw.

@andersonlthome
Copy link

Assuming updateRecipientInfo is a server action, the code provided is a dangling promise, whereas when you return it's an awaited promise.

startTransition(() => {
  const value = updateRecipientInfo(values);
  // `value` is a promise. It's not awaited/then'ed so it ends up throwing in the global scope.
});

The right way to go about it is this:

startTransition(async () => {
  await updateRecipientInfo(values);
});

In the experimental version of React that is enabled when you enable server actions async transitions are supported (it's what action uses under the hood too.

Hope that clarifies why it would not be caught when you "just call it" instead of returning. It's similar to not awaiting any other async function

works great!

@codekrafter
Copy link

I ran into the same issue and this fixes it, but it throws a type error. A transition function can only return void or undefined, and returning a Promise breaks that.

@timneutkens
Copy link
Member

A transition function can only return void or undefined

This is actually not the case when you enable server actions. The React experimental channel has support for Async Transitions, so an async function is supported in that case.

@RnbWd
Copy link

RnbWd commented Jul 25, 2023

it still doesn't work for me, and I've tried the following:

  • placing redirect outside of try / catch
  • wrapping server action with startTransition(() => action)
  • wrapping server action with startTransition(async() => {await action})
  • placing action outside of try / catch

edit: I discovered the issue I was having:

redirect() needs to exist outside of try / catch inside of the action, AND the action needs to exist outside of a try / catch inside of startTransition(). startTransition(async()) works well for me too, even though it's not documented officially yet. I need to return errors from action and set error state, which I'm able to do with startTransition(async()). Redirect is my success case, which throws inside of startTransition(async()), so the error state code is never called, which is fine.

@florianwalther-private
Copy link

florianwalther-private commented Jul 25, 2023

I need to return errors from action and set error state, which I'm able to do with startTransition(async())

Can you give an example of how you do this without wrapping the server action call into try/catch? And how is this related to the startTransition?

@codekrafter
Copy link

This is actually not the case when you enable server actions. The React experimental channel has support for Async Transitions, so an async function is supported in that case.

In my project with server actions enabled I'm still getting a type error, is there an extra step required to make the React typing in sync with the experimental channel enabled by Next?

@RnbWd
Copy link

RnbWd commented Aug 1, 2023

I need to return errors from action and set error state, which I'm able to do with startTransition(async())

Can you give an example of how you do this without wrapping the server action call into try/catch? And how is this related to the startTransition?

No the issue was that I cannot use a try / catch inside of startTransition. When I removed the try / catch statement, the redirect worked. The redirect inside of the action cannot be inside of a try / catch, and the action function on the frontend cannot be inside of a try / catch either

@github-actions
Copy link
Contributor

This closed issue has been automatically locked because it had no new activity for a month. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: app App directory (appDir: true) bug Issue was opened via the bug report template. locked
Projects
None yet
Development

No branches or pull requests