question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Errors during SSR are rendered as "loading"

See original GitHub issue

I’m writing a Next.js app that’s based on their with-apollo example and would like to share a confusion with the server-side error handling. I believe it might be either a bug in apollo client or something we can consider as a feature request.

In the example, the logic of a HOC that deals with Apollo data is located in with-apollo-client.js and mine is not very different. When the code runs on the server, getDataFromTree() is called and then the React tree renders using the data from apollo.cache.extract(). This works fine if all requests have succeeded – the server sends a fully rendered tree with all the data, that’s cool.

When a request fails during getDataFromTree(), the errors are suppressed with catch. This is generally fine, because we do not want a small faulty widget in a side panel to crash the whole page. However, because apollo.cache.extract() does not contain any information about the failed queries, failed query components render as if the data is still being loaded.

This peculiarity is a bit hard to spot in Next.js, because client-side rendering follows the server-side one and loading... gets replaced with a real error quickly (no data in cache → new query → render loading → register error → render error). However, this kind of “fix” makes the situation even harder to realise rather than resolved.

Imagine I have a page with a blog post, which is identified by a slug in the current URL. Unlike for a small side-widget I mentioned above, the success of a query that gets me the post is critical to the page. In this case, a failure means it’s either 404 or 500:

import { Query } from "react-apollo";

export default ({ slug }) => (
  <Query query={BLOG_POST_QUERY} variables={{ slug }}>
    {({ data, loading, error }) => {
      if (loading) {
        return <span>loading...</span>;
      }

      if (error) {
        // fail the whole page (500)
        throw error;
      }

      const blogPost = data && data.blogPost;
      if (!blogPost) {
        // fail the whole page (404 - blog post not found)
        const e = new Error("Blog post not found");
        e.code = "ENOENT";
        throw e;
      }
      return <BlogPostRepresentation blogPost={blogPost} />
    }}
  </Query>
);

The errors that are thrown here get handled by Next’s _error.js, which can render 404 - Post not found or 500 - App error, please reload using some custom logic. In neither case I want the server to return 200 even though the error will pop out on the client side shortly – search engines won’t like this. The code above renders a proper 404 page when data.blogPost is null, but if a graphql server goes down for some time, all my blog posts - existing or non-existing - will return 200 and this can quickly ruin google search results for my website.

How to reproduce the issue:

  1. Install with-apollo example

    npx create-next-app --example with-apollo with-apollo-app
    
  2. Add this line to components/PostList.js:

     function PostList ({
       data: { loading, error, allPosts, _allPostsMeta },
       loadMorePosts
     }) {
    +  console.log('RENDERING POST LIST', { loading, error: !!error, allPosts: !!allPosts });
       if (error) return <ErrorMessage message='Error loading posts.' />
    
  3. Launch it using yarn dev and open localhost:3000 in a browser. You will see:

    # server-side
    RENDERING POST LIST { loading: false, error: false, allPosts: true }
    RENDERING POST LIST { loading: false, error: false, allPosts: true }
    
    # client-side (uses cache, no loading)
    RENDERING POST LIST { loading: false, error: false, allPosts: true }
    RENDERING POST LIST { loading: false, error: false, allPosts: true }
    

    This looks fine.

  4. Simulate a graphql server failure (e.g. open lib/init-apollo.js and replace api.graph.cool with unavailable-host).

  5. Reload the page. You will still see Error loading posts., however your logs will say

    # server-side
    RENDERING POST LIST { loading: true, error: false, allPosts: false }
    
    # client-side (no cache found, loading from scratch and failing)
    RENDERING POST LIST {loading: true, error: false, allPosts: false}
    RENDERING POST LIST {loading: false, error: true, allPosts: false}
    

    (instead of loading: false, error: true on the server)

  6. Turn off javascript in the browser via dev tools and reload the page.
    Expected visible content: Error loading posts. Actual visible content: Loading

Versions

  System:
    OS: macOS High Sierra 10.13.6
  Binaries:
    Node: 10.9.0 - /usr/local/bin/node
    Yarn: 1.9.4 - /usr/local/bin/yarn
    npm: 6.2.0 - /usr/local/bin/npm
  Browsers:
    Chrome: 69.0.3497.81
    Firefox: 62.0
    Safari: 11.1.2
  npmPackages:
    apollo-boost: ^0.1.3 => 0.1.15 
    react-apollo: 2.1.0 => 2.1.0 
  npmGlobalPackages:
    apollo: 1.6.0

Meanwhile, my current workaround for critical server-side queries that are not allowed to fail will be something like this:

+ const isServer = typeof window === 'undefined';

  export default ({ slug }) => (
  <Query query={BLOG_POST_QUERY} variables={{ slug }}>
      {({ data, loading, error }) => {
+     // fail the whole page (500)
+     if ((isServer && loading) || error) {
+         throw error || new Error("500: Critical query failed");
+     }

      if (loading) {
          return <span>loading...</span>;
      }

-     if (error) {
-         // fail the whole page (500)
-         throw error;
-     }

      const blogPost = data && data.blogPost;
      if (!blogPost) {
          // fail the whole page (404 - blog post not found)
          const e = new Error("Blog post not found");
          e.code = "ENOENT";
          throw e;
      }
      return <BlogPostRepresentation blogPost={blogPost} />;
      }}
  </Query>
  );

I don’t see how I would render server-side 500 instead of 200 otherwise and I can also imagine that quite a few developers are not aware of what’s going on 🤔

WDYT folks?

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:33
  • Comments:20 (7 by maintainers)

github_iconTop GitHub Comments

19reactions
fenokcommented, Jul 15, 2019

And yet it still is.

6reactions
samueljuncommented, Nov 18, 2020

After spending a bunch of time debugging, I was able to figure out that the issue is specific to the getDataFromTree() SSR method whereas the renderToStringWithData() SRR method doesn’t have the issue and behaves as expected. Here are some sandboxes demonstrating the difference on apollo client v3 (I was also able to repro on v2):

👎 getDataFromTree() method sandbox: https://codesandbox.io/s/react-apollo-ssr-error-isnt-cached-getdatafromtree-method-upgraded-to-apolloclient3-p0k1y 👍 renderToStringWithData() method sandbox: https://codesandbox.io/s/react-apollo-ssr-error-isnt-cached-rendertostringwithdata-method-upgraded-to-apolloclient3-ohktp

getDataFromTree()

The getDataFromTree() method doesn’t have consistent behavior with errors because it doesn’t store errors in the apollo cache to be available for the final markup generation method (React’s renderToString()).

  • Essentially the getDataFromTree() function does two render passes. The first pass is to fetch data via executing all queries in the render tree, and the second pass to close out all the executing queries.
  • Once getDataFromTree() is done executing, we then need to run React’s renderToString() to generate the actual markup. You can see that there are 3 console.logs in the sandbox for each render pass (2 from getDataFromTree, 1 from renderToString).
  • In the success case, the reason that renderToString() is able to have the hydrated data from getDataFromTree() is because the apollo cache is hydrated with the success responses.
  • For the error case however, the apollo cache doesn’t store errors for whatever reason. So when React’s renderToString() method runs, the apollo react client doesn’t see the error and just returns a loading == true. This then produces the inconsistent server-side vs client-side situation that we’re seeing.

renderToStringWithData()

This is in contrast with the renderToStringWithData() method which works as expected. This is because renderToStringWithData() generates the markup right within the context of the second pass where the error is still in memory. You can see that there are only 2 console.logs in the sandbox for just the two passes required for executing the queries.

It looks like renderToStringWithData() is the more correct/consistent method for SSR until the apollo cache starts caching error for getDataFromTree() method to work as expected.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Logging and error management best practices in SSR apps
Handle errors and log them gracefully in your next web app with these best practice guidelines with examples using Next.js and Nuxt.js.
Read more >
Error when I make a server-side rendering - Stack Overflow
This usually happens when the component that you are using has asynchronous code that is not running on the server.
Read more >
Handling runtime errors when server side rendering with Next.js
This example considers a scenario when your webpage uses Server side rendering. The source code is spun out of a simple next.js boilerplate, ......
Read more >
Server-side rendering - Apollo GraphQL Docs
Server-side rendering (SSR) is a performance optimization for modern web apps. ... When you render your React app on the server side, most...
Read more >
react-hydration-error - Next.js
When css-in-js libraries are not set up for pre-rendering (SSR/SSG) it will often lead to a hydration mismatch. In general this means the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found