vercel / next.js

The React Framework
https://nextjs.org
MIT License
127.06k stars 27k forks source link

404 when redirecting in server action with subdomain rewrite rule #65893

Open lukemorton opened 6 months ago

lukemorton commented 6 months ago

Link to the code that reproduces this issue

https://github.com/lukemorton/next-testing

To Reproduce

  1. Add admin.localhost to /etc/hosts
  2. Run development server
  3. Navigate to http://admin.localhost:3000/a
  4. Submit form

Current vs. Expected behavior

When you submit the form, the redirect in the server action attempts to redirect to /b but 404s instead even though the URL exists. If you refresh the page works.

In the example attached there is also a <Link /> on /a which navigates fine to /b.

Appears to be an issue with redirect specifically in server actions.

Could not reproduce without the subdomain rewrite rule in next.config.mjs.

Could not reproduce when redirecting with relative URLs — changing from redirect("/b") to redirect("./b") fixes the issue.

I have been able to reproduce on Vercel with similar subdomain rewrite rule so not just related to localhost.

Expected behaviour: no 404 occurs when redirecting in a server action when I have rewrite rules in my application.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 22.1.0: Sun Oct  9 20:15:52 PDT 2022; root:xnu-8792.41.9~2/RELEASE_ARM64_T8112
  Available memory (MB): 24576
  Available CPU cores: 8
Binaries:
  Node: 20.13.1
  npm: 10.7.0
  Yarn: 1.22.19
  pnpm: N/A
Relevant Packages:
  next: 14.2.3 // Latest available version is detected (14.2.3).
  eslint-config-next: 14.2.3
  react: 18.3.1
  react-dom: 18.3.1
  typescript: 5.4.5
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Navigation

Which stage(s) are affected? (Select all that apply)

next dev (local), Vercel (Deployed)

Additional context

This issue started in 14.2.1. Appears to be fine in 14.1.3 and so we are holding back any further updates at our organisation.

igorxmath commented 5 months ago

I'm having the same issue with rewrites in middleware. Every time a redirect is called with server actions, Next.js shows a 404 page. However, when I reload the browser, the page loads correctly.

lukemorton commented 5 months ago

@igorxmath Good point about refreshing. A refresh after the 404 does indeed show the right page.

Only consistent fix I’ve found is to start redirect URL with a dot “./location” instead of “/location” or fully qualified domain, e.g. “https://example.com/location”. That’s if redirecting to another location on the same domain. I have not tested cross domain redirects after a server action.

jericopulvera commented 5 months ago

We encountered an issue when redirecting from nested path. e.g /users/create to /users

the fix is to add another dot similar to how you import file from a parent directory.

redirect("../users")

another example from /users/1/edit to /users

redirect("../../users")
jmarbutt commented 4 months ago

I am having this issue also. Was this working in previous versions? I feel like it was but in a recent update this changed. I am curious if anyone else has noticed that also?

I use redirect all over the place in my app with subdomain routing so I hate having to go into every one of my server actions to fix this but have for the time being.

RahulBirCodes commented 2 months ago

Are you guys getting responses from your server actions? I just get undefined because the middleware rewrites the calls server actions do under the hood but if I exclude it from the middleware then redirects don't work lol

EgorKrasno commented 2 months ago

I was also having this issue, it looks like when you redirect from a server action it sets the host header to just localhost:3000. Which breaks the middleware rewriting logic.

I was able to fix this in middleware by catching redirects originating from a server action and pulling the subdomain from x-forwarded-host and the redirect path from x-action-redirect and just rebuilding the URL.

const action = req.headers.get('x-action-redirect');
if (process.env.NODE_ENV === 'production' && action) {
    const forwardedHost = req.headers.get('x-forwarded-host')?.split('.')[0];
    const newUrl = `https://localhost:3000/${forwardedHost}/${action.split('/').pop()}`;
    return NextResponse.rewrite(newUrl);
}
FroeMic commented 2 months ago

@EgorKrasno I faced a similar issue, using a server-action to log-in users in multi-tenant app that handles subdomains via rewrites in a middleware. Your answer put me on the right path.

I adapted you code, right at the top of the middleware function. I found that the x-action-redirect property contained the desired url, which allowed me tom avoid some of the splitting.

export default async function middleware(req: NextRequest) {
  let host = req.headers.get("host")!;
  let url = new URL(req.url);

 //------------------------------------------------------------
 // Relevant code starts here
 //-----------------------------------------------------------

const action = req.headers.get("x-action-redirect");
const forwardedHost = req.headers.get("x-forwarded-host")!;
  if (
    process.env.NODE_ENV === "production" &&
    host == "localhost:3000" &&
    forwardedHost &&
    action
  ) {
    host = forwardedHost;
    url = new URL(action, req.url);
  }

 //------------------------------------------------------------
 // Relevant code ends here
 //-----------------------------------------------------------`

  // Get hostname of request (e.g. demo.vercel.pub, demo.localhost:3000)
  const hostname = getTenantHostname(host);
  const subdomain = getTenantSubdomain(host);

  const searchParams = url.searchParams.toString();
  // Get the pathname of the request (e.g. /, /about, /blog/first-post)
  const path = `${url.pathname}${
    searchParams.length > 0 ? `?${searchParams}` : ""
  }`;

  // Deal with reserved subdomains (e.g. admin.domain.tld)
  if (hostname == `admin.${env.PUBLIC_ROOT_DOMAIN}`) {
    return NextResponse.rewrite(
      new URL(`/admin${path === "/" ? "" : path}`, req.url),
    );
  }

  // Deal with the case when there is NO subdomain; i.e. redirect to a landing page
  if (hostname === "localhost:3000" || hostname === env.PUBLIC_ROOT_DOMAIN) {
    return NextResponse.rewrite(
      new URL(`/home${path === "/" ? "" : path}`, req.url),
    );
  }

  // rewrite everything else to `/[subdomain]/[slug] dynamic route
  return NextResponse.rewrite(new URL(`/${subdomain}${path}`, req.url));
}

For completeness, here are the other two custom functions:

export function getTenantHostname(host: string) {
  // host e.g.: calw.localhost:3000, calw.climate-hub.eu, climae-hub.eu, 11---preview.calw.climate-hub.eu

  let hostname = host.replace(".localhost:3000", `.${env.PUBLIC_ROOT_DOMAIN}`); // the "replace only works if there is a subdomain present"

  // Deal with deployment previews
  if (hostname.includes("---preview.")) {
    hostname = `${hostname.split("---preview.")[1]}`;
  }

  return hostname;
}

export function getTenantSubdomain(host: string) {
  const subdomain = getTenantHostname(host);
  return subdomain.replace(`.${env.PUBLIC_ROOT_DOMAIN}`, "");
}

Thank you for your answer! Hope this helps someone.