-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Description
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:
-
Install
with-apolloexamplenpx create-next-app --example with-apollo with-apollo-app
-
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.' /> -
Launch it using
yarn devand openlocalhost:3000in 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.
-
Simulate a graphql server failure (e.g. open
lib/init-apollo.jsand replaceapi.graph.coolwithunavailable-host). -
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: trueon the server) -
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?