]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(ssr): improve ssr async setup / suspense error handling
authorEvan You <yyx990803@gmail.com>
Tue, 10 Mar 2020 19:28:13 +0000 (15:28 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 10 Mar 2020 20:52:31 +0000 (16:52 -0400)
packages/runtime-core/src/component.ts
packages/server-renderer/__tests__/ssrSuspense.spec.ts
packages/server-renderer/src/renderToString.ts

index ad8453e9c0f30d47169d288a27c046a34f84e800..05112c70f74a4521437518570644caf1f891385f 100644 (file)
@@ -337,7 +337,7 @@ function setupStatefulComponent(
   // 2. create props proxy
   // the propsProxy is a reactive AND readonly proxy to the actual props.
   // it will be updated in resolveProps() on updates before render
-  const propsProxy = (instance.propsProxy = isInSSRComponentSetup
+  const propsProxy = (instance.propsProxy = isSSR
     ? instance.props
     : shallowReadonly(instance.props))
   // 3. call setup()
@@ -360,7 +360,7 @@ function setupStatefulComponent(
     currentSuspense = null
 
     if (isPromise(setupResult)) {
-      if (isInSSRComponentSetup) {
+      if (isSSR) {
         // return the promise so server-renderer can wait on it
         return setupResult.then(resolvedResult => {
           handleSetupResult(instance, resolvedResult, parentSuspense, isSSR)
index b411e14a7803a8412d9f29fa3551d542f33e7a4b..c4c3b2d5a9fe62c8e0b90ec847bcf79f31323026 100644 (file)
@@ -2,6 +2,16 @@ import { createApp, h, Suspense } from 'vue'
 import { renderToString } from '../src/renderToString'
 
 describe('SSR Suspense', () => {
+  let logError: jest.SpyInstance
+
+  beforeEach(() => {
+    logError = jest.spyOn(console, 'error').mockImplementation(() => {})
+  })
+
+  afterEach(() => {
+    logError.mockRestore()
+  })
+
   const ResolvingAsync = {
     async setup() {
       return () => h('div', 'async')
@@ -10,7 +20,7 @@ describe('SSR Suspense', () => {
 
   const RejectingAsync = {
     setup() {
-      return new Promise((_, reject) => reject())
+      return new Promise((_, reject) => reject('foo'))
     }
   }
 
@@ -25,6 +35,7 @@ describe('SSR Suspense', () => {
     }
 
     expect(await renderToString(createApp(Comp))).toBe(`<div>async</div>`)
+    expect(logError).not.toHaveBeenCalled()
   })
 
   test('fallback', async () => {
@@ -38,6 +49,7 @@ describe('SSR Suspense', () => {
     }
 
     expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
+    expect(logError).toHaveBeenCalled()
   })
 
   test('2 components', async () => {
@@ -53,6 +65,7 @@ describe('SSR Suspense', () => {
     expect(await renderToString(createApp(Comp))).toBe(
       `<div><div>async</div><div>async</div></div>`
     )
+    expect(logError).not.toHaveBeenCalled()
   })
 
   test('resolving component + rejecting component', async () => {
@@ -66,6 +79,7 @@ describe('SSR Suspense', () => {
     }
 
     expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
+    expect(logError).toHaveBeenCalled()
   })
 
   test('failing suspense in passing suspense', async () => {
@@ -87,6 +101,7 @@ describe('SSR Suspense', () => {
     expect(await renderToString(createApp(Comp))).toBe(
       `<div><div>async</div><div>fallback 2</div></div>`
     )
+    expect(logError).toHaveBeenCalled()
   })
 
   test('passing suspense in failing suspense', async () => {
@@ -106,5 +121,6 @@ describe('SSR Suspense', () => {
     }
 
     expect(await renderToString(createApp(Comp))).toBe(`<div>fallback 1</div>`)
+    expect(logError).toHaveBeenCalled()
   })
 })
index 8051c204b4fd9ea30806373f94545fc367cf7fa4..ee9dd8143d30736c767ea6f2010a29f4d6221ba0 100644 (file)
@@ -10,7 +10,6 @@ import {
   Fragment,
   ssrUtils,
   Slots,
-  warn,
   createApp,
   ssrContextKey
 } from 'vue'
@@ -139,6 +138,8 @@ export function renderComponent(
   )
 }
 
+export const AsyncSetupErrorMarker = Symbol('Vue async setup error')
+
 function renderComponentVNode(
   vnode: VNode,
   parentComponent: ComponentInternalInstance | null = null
@@ -150,7 +151,21 @@ function renderComponentVNode(
     true /* isSSR */
   )
   if (isPromise(res)) {
-    return res.then(() => renderComponentSubTree(instance))
+    return res
+      .catch(err => {
+        // normalize async setup rejection
+        if (!(err instanceof Error)) {
+          err = new Error(String(err))
+        }
+        err[AsyncSetupErrorMarker] = true
+        console.error(
+          `[@vue/server-renderer]: Uncaught error in async setup:\n`,
+          err
+        )
+        // rethrow for suspense
+        throw err
+      })
+      .then(() => renderComponentSubTree(instance))
   } else {
     return renderComponentSubTree(instance)
   }
@@ -208,7 +223,9 @@ function ssrCompile(
     isNativeTag: instance.appContext.config.isNativeTag || NO,
     onError(err: CompilerError) {
       if (__DEV__) {
-        const message = `Template compilation error: ${err.message}`
+        const message = `[@vue/server-renderer] Template compilation error: ${
+          err.message
+        }`
         const codeFrame =
           err.loc &&
           generateCodeFrame(
@@ -216,7 +233,7 @@ function ssrCompile(
             err.loc.start.offset,
             err.loc.end.offset
           )
-        warn(codeFrame ? `${message}\n${codeFrame}` : message)
+        console.error(codeFrame ? `${message}\n${codeFrame}` : message)
       } else {
         throw err
       }
@@ -243,15 +260,15 @@ function renderVNode(
       break
     default:
       if (shapeFlag & ShapeFlags.ELEMENT) {
-        renderElement(push, vnode, parentComponent)
+        renderElementVNode(push, vnode, parentComponent)
       } else if (shapeFlag & ShapeFlags.COMPONENT) {
         push(renderComponentVNode(vnode, parentComponent))
       } else if (shapeFlag & ShapeFlags.PORTAL) {
-        renderPortal(vnode, parentComponent)
+        renderPortalVNode(vnode, parentComponent)
       } else if (shapeFlag & ShapeFlags.SUSPENSE) {
-        push(renderSuspense(vnode, parentComponent))
+        push(renderSuspenseVNode(vnode, parentComponent))
       } else {
-        console.warn(
+        console.error(
           '[@vue/server-renderer] Invalid VNode type:',
           type,
           `(${typeof type})`
@@ -270,7 +287,7 @@ export function renderVNodeChildren(
   }
 }
 
-function renderElement(
+function renderElementVNode(
   push: PushFn,
   vnode: VNode,
   parentComponent: ComponentInternalInstance
@@ -325,17 +342,17 @@ function renderElement(
   }
 }
 
-function renderPortal(
+function renderPortalVNode(
   vnode: VNode,
   parentComponent: ComponentInternalInstance
 ) {
   const target = vnode.props && vnode.props.target
   if (!target) {
-    console.warn(`[@vue/server-renderer] Portal is missing target prop.`)
+    console.error(`[@vue/server-renderer] Portal is missing target prop.`)
     return []
   }
   if (!isString(target)) {
-    console.warn(
+    console.error(
       `[@vue/server-renderer] Portal target must be a query selector string.`
     )
     return []
@@ -367,7 +384,7 @@ async function resolvePortals(context: SSRContext) {
   }
 }
 
-async function renderSuspense(
+async function renderSuspenseVNode(
   vnode: VNode,
   parentComponent: ComponentInternalInstance
 ): Promise<ResolvedSSRBuffer> {
@@ -375,10 +392,15 @@ async function renderSuspense(
   try {
     const { push, getBuffer } = createBuffer()
     renderVNode(push, content, parentComponent)
+    // await here so error can be caught
     return await getBuffer()
-  } catch {
-    const { push, getBuffer } = createBuffer()
-    renderVNode(push, fallback, parentComponent)
-    return getBuffer()
+  } catch (e) {
+    if (e[AsyncSetupErrorMarker]) {
+      const { push, getBuffer } = createBuffer()
+      renderVNode(push, fallback, parentComponent)
+      return getBuffer()
+    } else {
+      throw e
+    }
   }
 }