NextJS behind proxy with URL Rewrite
Asked Answered
D

3

13

Context:

I've a NextJS deployment behind Nginx. The idea is to use NextJS to create several websites hosted in different domains. Each domain will have an entry in Nginx and it will be pointing to the specific path pages/cafes/[cafeId] in NextJS. There will be only one NextJs deployment for all websites and each domain will be routed using static proxy in nginx.

nginx.conf

   server {
        listen 80;
        server_name www.cafe-one.local;
        location = / {
            proxy_pass http://localhost:3000/cafes/cafe_id_1;
            ...
        }
        location / {
            proxy_pass http://localhost:3000/;
            ...
        }
    }
    
    server {
        listen 80;
        server_name www.cafe-two.local;
        location = / {
            proxy_pass http://localhost:3000/cafes/cafe_id_2;
            ...
        }
        location / {
            proxy_pass http://localhost:3000/;
            ...
        }
    }

pages/[cafeId]/index.js

export const getStaticPaths = async () => {
  return {
    paths: [], // no website rendered during build time, there are around 1000+ sites
    fallback: true
  };
};

export const getStaticProps = async context => {

  const cafeId = context.params.cafeId;
  const cafe = await ... // get data from server

  return {
    props: {
      cafe
    },
    revalidate: 10 // revalidate every 10 seconds
  };
};

export default function CafeWebsite(props) {
  const router = useRouter();

  // getStaticProps() is not finished
  if (router.isFallback) {
    return <div>Loading...</div>;
  }
  
  return <div>{props.cafe.name}</div>;
}

Issue:

When I access www.cafe-one.local, i get to the loading screen, but then NextJS throw a client-side error about the The provided as value (/) is incompatible with the href value (/cafes/[cafeId]). This is understandable because the current URL is not what NextJS is expecting.

enter image description here

Question:

How to fix the issue, such that NextJS could be used in front of Nginx reverse proxy?

Any help is appreciated.

Thanks in advance.

Dowse answered 1/8, 2020 at 4:2 Comment(2)
I'm in the same boat, have you found any solution?Girvin
Also running into the same problem.. very curious if you ever found a solution.Leakey
A
5

Edit Oct 2021: Next.js 12 now supports middleware, which can achieve dynamic subdomains without the need for a proxy server. See an official example.


Original answer: Thanks to Ale for the replaceState idea, this is how we handle it now:

  1. Nginx proxies all initial requests to our vercel deployment with /original.host.com/ as the first item in the path:
server {
    listen 8080;

    # redirect HTTP to HTTPS
    if ($http_x_forwarded_proto = "http") {
        return 301 https://$host$request_uri;
    }

    # (not needed if you set assetPrefix in next.config.js to https://myapp.com)
    location /_next {
        proxy_pass https://myapp.com;
    }

    # a separate line for root is needed to bypass nginx messing up the request uri
    location = / {
        proxy_pass https://myapp.com/$host;
    }

    # this is the primary proxy
    location / {
        proxy_pass https://myapp.com/$host$request_uri;
    }
}
  1. In _app.tsx, we register an effect that runs after Next.js finishes changing the route (this isn't triggered on the first render).
useEffect(() => {
  const handleRouteChange = (url: string) => {
    const paths = url.split('/')

    if (paths[1] === location.host) {
      // remove /original.host.com/ from the path
      // note that passing history.state as the first argument makes back/forward buttons work correctly
      history.replaceState(history.state, '', `/${paths.slice(2).join('/')}`)
    }
  }

  router.events.on('routeChangeComplete', handleRouteChange)

  return () => {
    router.events.off('routeChangeComplete', handleRouteChange)
  }
}, [])
  1. All domain-specific pages are under pages/[domain]/, e.g. pages/[domain]/mypage.tsx.

  2. Finally, we prepend the original hostname to each href, like

    • <Link href="/original.host.com/mypage">...</Link> or
    • Router.push('/original.host.com/mypage').

    There's no need to use as anymore.

  3. Next.js will now navigate to https://original.host.com/original.host.com/mypage for a split second, and then replace it with https://original.host.com/mypage once the transition is complete.

  4. To make SSR work for each domain-specific page, we add getStaticPaths/getStaticProps in each of our pages so that Next.js knows to generate a separate version of the page for each domain (otherwise router.query.domain will be empty while in SSR and we'd get errors that the paths mismatch). Note that this doesn't negatively affect performance because the page will be cached after the first request.

// In pages/[domain]/mypage.tsx

export default function MyPage(params: MyPageProps) {
  const router = useRouter()
  // SSR provides domain as a param, client takes it from router.query
  const domain = params.domain || router.query.domain

  // ... rest of your page
}

export const getStaticPaths: GetStaticPaths = async () => ({
  fallback: 'blocking',
  paths: [],
})

export const getStaticProps: GetStaticProps<SpaceEditPageProps> = async ({
  params,
}) => {
  return {
    props: {
      domain: params?.domain as string,
    },
  }
}
  1. Bonus tip: To see the final URL when the user hovers a Link, we don't use passHref but instead set the fake URL as href, cancel default action in onClickCapture, and trigger the router in onClick (you can extract this into a reusable component):
<Link href="/original.host.com/mypage">
  <a
    href="/mypage"
    onClick={() => Router.push("/original.host.com/mypage")}
    onClickCapture={e => e.preventDefault()}
  >
    // ...
  </a>
</Link>
Accustomed answered 23/3, 2021 at 17:19 Comment(1)
Wow.. thank you so much - have spent almost the whole day to figure out that nginx needs that second / to prevent messing up the URL. I still don't really get why though. Could you shine some light on this? Thank you so much - saved the day!Guenon
N
1

I have been dealing with the same issue, but for mapping different subdomains to a dynamic route in the NextJS app.

I haven't been able to find a proper solution to the The provided as value (/) is incompatible with the href value error, but I found a somewhat hacky workaround.

First, you have to redirect the requests from my-domain.com to my-domain.com/path-to-dynamic-route. Then you have to reverse proxy all request from my-domain.com/path-to-dynamic-route to the same dynamic route in the NextJS app, like localhost:3000/path-to-dynamic-route.

You can do it manually from NGINX with a combination of return 301 and proxy_pass, or you can let NextJS do it automatically by passing the dynamic route in the proxy_pass directive with a trailing slash.

nginx.conf


server {
    listen 80;
    server_name www.cafe-one.local;
    location = / {
        # When a url to a route has a trailing slash, NextJS responds with a "308 Permanent redirect" to the path without the slash.
        # In this case from /cafes/cafe_id_1/ to /cafes/cafe_id_1
        proxy_pass http://localhost:3000/cafes/cafe_id_1/;
        # If you also want to be able to pass parameters in the query string, you should use the variable $request_uri instead of "/"
        # proxy_pass http://localhost:3000/cafes/cafe_id_1$request_uri;
        ...
    }
    location / {
        # Any other request to www.cafe-one.local will keep the original route and query string
        proxy_pass http://localhost:3000$request_uri;
        ...
    }
}

This should work, but now we have a problem with the url in the address bar. Any user visiting www.cafe-one.local will be redirected to www.cafe-one.local/cafes/cafe_id_1 and that doesn't look nice.

The only workaround I found to solve this issue was to use javascript to remove the path by rewriting the browsing history with window.history.replaceState().

pages/[cafeId]/index.js

...
export default function CafeWebsite(props) {
  if (typeof window !== "undefined") {
    window.history.replaceState(null, "", "/")
  }
...

if you don't want to remove the path for all domains, you can use window.location.hostname to check the current url.

...
export default function CafeWebsite(props) {
  if (typeof window !== "undefined") {
    const hostname = window.location.hostname
    const regex = /^(www\.my-domain\.|my-domain\.)/
    if (!regex.test(hostname)) {
      window.history.replaceState(null, "", "/")
    }
  }
...
Nordstrom answered 20/9, 2020 at 0:49 Comment(1)
Thanks Ale for your reply. Something I've understood working with NextJS is that the URL on the client-side and server-side is tightly integrated, because of the SSR nature of the nextJS app, any attempts to disconnect these 2 leads you down the rabbit-hole.Dowse
B
1

I've managed to handle the same issue by forcing my dynamic pages to use ONLY serverside rendering.

The fact is, Next tries to hydrate route params from the browser page URL which is outside of Next's context due to nginx.

Brunei answered 4/9, 2021 at 11:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.