vercel / next.js

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

getStaticProps is not called at request time when preview cookies are set #19714

Closed justintemps closed 3 years ago

justintemps commented 4 years ago

Bug report

The docs state that when preview mode cookies are set via res.setPreviewData then getStaticProps will be called at request time instead of at build time. But I can't get this to work reliably in production in dynamic routes with getStaticPaths. The cookies are set correctly, but getStaticProps does not get called.

Describe the bug

I have a headless CMS (Prismic) that generates a preview url that looks like this:

https://my.website.com/api/preview?token={token}&documentId={documentID}

There I have the /api/preview handler that redirects to the appropriate content and sets the preview cookies __next_preview_data and __prerender_bypass

LocalHost/Development All pages are correctly loaded in Preview Mode:

LocalHost/Production All pages are correctly loaded in Preview Mode In FireFox but not in Chrome. In Chrome, static routes appear in Preview mode whereas dynamic routes do not. Preview cookies are set correctly.

Server/Production Only static routes appear in Preview Mode for both FireFox and Chrome. Dynamic routes do not appear in Preview Mode even though preview cookies are set.

To Reproduce

// package.json

{
...
   "scripts": {
        "dev": "next-translate && next dev",
        "build": "next-translate && next build",
        "start": "next start -p 8080",
    }
}
// next.config.js

const path = require("path");
const nextBuildId = require("next-build-id");
const { locales, defaultLocale } = require("./i18n.json");

const nextConfig = {
  i18n: { locales, defaultLocale, localeDetection: false },
  images: {
    domains: ["my-domain.com", "sub.my-domain.com"],
  },
  generateBuildId: () => nextBuildId({ dir: __dirname }),
};

module.exports = nextConfig;
// pages/api/preview.ts

import { Client, linkResolver } from "../../prismic-config";

const Preview = async (req: any, res: any) => {
  const { token: ref, documentId } = req.query;
  const redirectUrl = await Client(req)
    .getPreviewResolver(ref, documentId)
    .resolve(linkResolver, "/");

  if (!redirectUrl) {
    return res.status(401).json({ message: "Invalid token" });
  }

  res.setPreviewData({ ref });
  res.write(
    `<!DOCTYPE html><html><head><meta http-equiv="Refresh" content="0; url=${redirectUrl}" />
    <script>window.location.href = '${redirectUrl}'</script>
    </head>`
  );
  res.end();
};

export default Preview;
// pages/posts/[uid].tsx

import { GetStaticPropsContext } from "next";
import { Document } from "prismic-javascript/types/documents";
import React from "react";
import Prismic from "prismic-javascript";
import { useRouter } from "next/router";
import { Client } from "../prismic-config";

interface StaticPropsContext extends GetStaticPropsContext {
  params: {
    uid: string;
  };
}

interface Props {
  story: Document;
  preview: boolean;
}

const Story: React.FC<Props> = ({ preview, story }) => {
  const router = useRouter();
  if (router.isFallback) {
    return <div>"...Loading"</div>;
  }

  return (
    <>
      <h1>Example</h1>
      <main>
        <pre>
          <code>{JSON.stringify(story, null, 4)}</code>
        </pre>
        {preview && <span>This is a preview</span>}
      </main>
    </>
  );
};

export default Story;

export async function getStaticPaths() {
  const client = Client();
  const stories = await client.query(
    Prismic.Predicates.at("document.type", "story"),
    { lang: "*" }
  );
  const paths = stories.results.map(({ uid, lang = undefined }) => ({
    params: {
      uid,
    },
    locale: lang,
  }));
  return { paths, fallback: true };
}

export async function getStaticProps({
  params,
  locale = "en-gb",
  preview = false,
  previewData = {},
}: StaticPropsContext) {
  const { ref } = previewData;
  const { uid } = params;

  const client = Client();
  const story = await client.getByUID("story", uid, {
    ref,
    lang: locale,
    fetchLinks: ["author.name", "author.image"],
  });

  return { props: { story, preview } };
}

Expected behavior

Preview cookies should result in getStaticProps being called at request time.

System information

justintemps commented 3 years ago

I did some more investigating and I believe this is caused by aggressive caching for apps that are being served behind Cloudflare. It would be fine if you could set caching headers as explained here, however those headers in particular are overridden by Next.js so that configuring Cache-Control has no effect. I'm opening a separate issue about that as I think it would solve this issue.

tremby commented 3 years ago

Cloudflare is not the culprit, or at least not the only one. On my local machine, where there is certainly no Cloudflare involved:

_ next dev next start
Firefox preview works preview works
Chrome preview works preview does not work

...unless you're saying Prismic itself is using Cloudflare?

justintemps commented 3 years ago

@tremby it may not be the only culprit, I can get preview working in Chrome on the server but only for my / route where Next.js sets

Cache-Control: s-maxage=1, stale-while-revalidate

On dynamic routes like /posts/[:uid] it uses

Cache-Control: s-maxage=31536000, stale-while-revalidate
tremby commented 3 years ago

Is your server running HTTPS?

I just did my own digging and the culprit in my case is that in next start mode Next's setPreviewData function results in some Set-Cookie headers with the Secure flag (and SameSite=None which I'm not sure of the purpose of but mentioning it anyway), while in next dev mode those cookies are set without the Secure flag (and with SameSite=Lax).

Firefox doesn't seem to care (at least for localhost? not sure if an exception or it just doesn't honour Secure at all), but Chrome gives a warning on both Set-Cookie attempts:

This Set-Cookie was blocked because it had the "Secure" attribute but was not received over a secure connection.

So in next start mode without HTTPS the cookies aren't set in Chrome, and so preview mode doesn't work.

tremby commented 3 years ago

Oh, and that's super weird that it'd set different max-age hints for different routes, assuming you didn't ask it to.

tremby commented 3 years ago

It looks like I just needed to set NODE_ENV to development, and now everything is working on my end in both browsers and modes.

Sorry for the noise -- my issue was not the same as yours! My comments here were irrelevant to your bug report; say the word and I'll delete them.

tremby commented 3 years ago

My project isn't in production yet. My client and I were finding this issue on local machines when testing. In production we'll be using HTTPS of course, and so I don't anticipate seeing the same issue. Maybe at that point I'll see the bug you're seeing though. :)

tremby commented 3 years ago

Just ran NODE_ENV=development npm start.

justintemps commented 3 years ago

Ok, so running NODE_ENV=development npm start also resolves this issue on my local machine, but not in production where requests with the cookie are not reaching the server because they're getting cached in Cloudflare.

justintemps commented 3 years ago

So, I've got this find on my end and it turned out there were two issues:

On my local machine With next start, Preview Mode worked in Chrome but not FireFox. This is probably because of the Next.js preview cookies were set with a secure flag. In any event, running NODE_ENV=development npm start fixed it.

On the server With next start, Preview Mode only worked on the first request, because afterwards the normal page were getting cached in Cloudflare and successive requests weren't reaching the server. I was able to fix this by setting revalidate in getStaticProps to 1.

I hope this helps anyone who runs into the same problem.

webholics commented 3 years ago

Setting revalidate to 1 effectively disables the cache in production which seems like a very bad fix. We are currently also having a very hard time to find a way how to do this right.

Seems like previews are only really doable on a development instance but not on production.

tremby commented 3 years ago

A hacky workaround might be to adjust your preview route's link resolver so that it adds a random query string to the end of the URL.

justintemps commented 3 years ago

@webholics I came to the same conclusion, I'm only enabling preview mode on my staging instance. However, you don't have to run next in development mode to do this, you can simply disable caching on your staging instance with an env var or something. I think that's preferable as Next.js behaviour changes a lot between prod and dev modes.

tremby commented 3 years ago

...or to adjust your CDN settings so that it will vary the response based on the preview data cookie. This would be much less hacky. I believe this is possible with Cloudfront. Not sure about Cloudflare.

webholics commented 3 years ago

Actually we use next export and there the support for doing previews is even worse. So it seems we now need a separately running node server just to be able to support previews.

justintemps commented 3 years ago

@webholics I don't think getStaticProps is called on the client, so you would probably have to have that anyway (or a serverless setup that took care of that for you)

webholics commented 3 years ago

true

tremby commented 3 years ago

Actually we use next export and there the support for doing previews is even worse

Oh, that's a shame. I thought one of Next's whole things was that the /pages/api/* things could be turned into lambdas. Can you not do that with next export...?

justintemps commented 3 years ago

@tremby I think Vercel will do this for you without any customization. Otherwise you need a serverless wrapper like next-on-netlify which converts all of your api routes to lambdas.

tremby commented 3 years ago

Damn I guess not, since all the preview route does is set cookies and then redirect you. The static site I'm building may be a whole lot less static than we intended.

balazsorban44 commented 2 years ago

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.