Repository: FormidableLabs/urql (ts)

Issue:
Inconsistent error output on SSR rehydration
## Description

1\) 404 cases lead to different SSR and Browser representations of `error`.
2\) SSR representation seems to be slightly off.

### Steps to reproduce

1) Access 404 API from Browser directly (trigger SSR).
2) Access 404 API from Browser via link (trigger client fetching).

By 404 API I mean any API returning `message: "Not Found", code: INTERNAL_SERVER_ERROR` which is probably not perfect but is the default ApolloServer behavior:

> AuthenticationError — for authentication failures
> ForbiddenError — for authorization failures
> UserInputError — for validation errors on user input
> **As a fallback for uncaught failures, any other unknown errors thrown within your resolver will add the code INTERNAL_SERVER_ERROR**
> https://blog.apollographql.com/full-stack-error-handling-with-graphql-apollo-5c12da407210

and shouldn't really matter for the context of this issue.

### Code

```
// some NextJS page

let [dataResult] = useQuery({
  query,
})

if (dataResult.error) {
  console.log(dataResult.error) // !!!
  return <Error message={dataResult.error.toString()}/>
}
```

### SSR Error

```
[CombinedError: [Network] undefined] {
  name: 'CombinedError',
  message: '[Network] undefined',
  graphQLErrors: [
    GraphQLError: Not Found
        at rehydrateGraphQlError (/.../node_modules/urql/dist/cjs/index-dddbd19e.js:23:12)
        at Array.map (<anonymous>)
        at new CombinedError (/.../node_modules/urql/dist/cjs/index-dddbd19e.js:39:61)
        ...
        at b (/.../node_modules/wonka/dist/wonka.js:865:14)
        at Array.forEach (<anonymous>) {
      message: 'Not Found'
    }
  ],
  networkError: Error: undefined
      at /.../node_modules/urql/dist/cjs/core.js:525:25
      ...
      at d (/.../node_modules/wonka/dist/wonka.js:137:16),
  response: undefined
}
```

`error.toString()` currently produces (at server) `[Network] undefined`.
Somehow it misses the informative ` GraphQLError: Not Found` part and goes for the weird  `[Network] undefined`

### Browser

Talking about the same error in Browser.

`error.toString()` currently produces `[GraphQL] Not Found`.
Which LGTM.

---

The same error should probably lead to the same structures in Browser and Server.
At least, the error in Server should be improved to match the Browser one. 
Stacktraces will be different for sure, but the final messages of `toString` should match IMO.

Pull request description:
Fix error serializer/rehydrator in ssrExchange
Fix #508

In all error cases the ssrExchange would be generated
while assuming that networkError may never be undefined.
This could cause an edge case where networkError was
set to Error('undefined') on the client-side.

Patch:
diff --git a/docs/guides.md b/docs/guides.md
index e6943ed2..621586a7 100644
--- a/docs/guides.md
+++ b/docs/guides.md
@@ -465,7 +465,7 @@ import { extractFiles } from 'extract-files';
 
 ...
 const createFetchSource = (operation) => {
-  return make(([next, complete]) => {
+  return make(({next, complete}) => {
     const { context } = operation;
     const { clone, files } = extractFiles(operation.variables);
     const isFileUpload = !!files.size;
diff --git a/src/exchanges/ssr.ts b/src/exchanges/ssr.ts
index ff5d73b2..ccb406a5 100644
--- a/src/exchanges/ssr.ts
+++ b/src/exchanges/ssr.ts
@@ -37,8 +37,8 @@ const serializeResult = ({
   const result: SerializedResult = { data, error: undefined };
   if (error !== undefined) {
     result.error = {
-      networkError: '' + error.networkError,
-      graphQLErrors: error.graphQLErrors.map(x => '' + x),
+      graphQLErrors: error.graphQLErrors.map(x => x.message),
+      networkError: error.networkError ? '' + error.networkError : undefined,
     };
   }
 
@@ -55,14 +55,18 @@ const deserializeResult = (
     operation,
     data,
     extensions: undefined,
-    error: undefined,
+    error: error
+      ? new CombinedError({
+          networkError: error.networkError
+            ? new Error(error.networkError)
+            : undefined,
+          graphQLErrors:
+            error.graphQLErrors && error.graphQLErrors.length
+              ? error.graphQLErrors
+              : undefined,
+        })
+      : undefined,
   };
-  if (error !== undefined) {
-    deserialized.error = new CombinedError({
-      networkError: new Error(error.networkError),
-      graphQLErrors: error.graphQLErrors,
-    });
-  }
 
   return deserialized;
 };


Test patch:
diff --git a/src/exchanges/ssr.test.ts b/src/exchanges/ssr.test.ts
index c2b0f37d..7c4f81c1 100644
--- a/src/exchanges/ssr.test.ts
+++ b/src/exchanges/ssr.test.ts
@@ -3,6 +3,7 @@ import { makeSubject, pipe, map, publish, forEach, Subject } from 'wonka';
 import { Client } from '../client';
 import { queryOperation, queryResponse } from '../test-utils';
 import { ExchangeIO, Operation } from '../types';
+import { CombinedError } from '../utils';
 import { ssrExchange } from './ssr';
 
 let forward: ExchangeIO;
@@ -40,6 +41,36 @@ it('caches query results correctly', () => {
   });
 });
 
+it('caches errored query results correctly', () => {
+  output.mockReturnValueOnce({
+    ...queryResponse,
+    data: null,
+    error: new CombinedError({
+      graphQLErrors: ['Oh no!'],
+    }),
+  });
+
+  const ssr = ssrExchange();
+  const { source: ops$, next } = input;
+  const exchange = ssr(exchangeInput)(ops$);
+
+  publish(exchange);
+  next(queryOperation);
+
+  const data = ssr.extractData();
+  expect(Object.keys(data)).toEqual(['' + queryOperation.key]);
+
+  expect(data).toEqual({
+    [queryOperation.key]: {
+      data: null,
+      error: {
+        graphQLErrors: ['Oh no!'],
+        networkError: undefined,
+      },
+    },
+  });
+});
+
 it('resolves cached query results correctly', () => {
   const onPush = jest.fn();