

[StackOverflow本家にも質問](https://stackoverflow.com/questions/61731472/when-an-
error-occurs-in-react-ssr-what-kind-of-response-should-be-
returned)を投げていますが、回答がつかないのでこちらでも質問させてください。  
もし有用な回答があった場合は両方に反映させます。

## 用語

  * SPA = Single Page Application
  * SSR = Server Side Rendering

## 実現したいこと

expressとReactでSSRを実装した時、レンダリング中にエラーが投げられたとします。  
エラーをキャッチしたタイミングで以下のデータを返すことを考えます。

  * ステータスコード500を返す
  * エラーの種類を判別するためのカスタムヘッダーを付ける

さらに、このアプリケーションは複雑なため、レンダリングのパフォーマンスを向上させるためクライアントに渡すデータは、送信可能なものから順番にストリームで送信していこうと考えています。そのため、SSRでエラーが発生する前にHTTPのレスポンスボディが到達する可能性があります。

例えば次のものが挙げられます。

  * `<!DOCTYPE html>`
  * 固定の`<head>`タグの内容

## サンプルコード

これらの要求を再現するために、`renderToStaticNodeStream`と`renderToStaticMarkup`を用いた2つの実装例を提供します。

GitHub: <https://github.com/Himenon/react-ssr-error-handle>

[code]

    import * as React from "react";
    import express from "express";
    import {
      renderToStaticNodeStream,
      renderToStaticMarkup,
    } from "react-dom/server";
    
    const SERVER_PORT = 9000;
    
    const server = express();
    
    const LargeApplication = () => {
      const somethingError = () => {
        console.log(`Access window object in nodejs: ${window.location.href}`);
      };
      somethingError();
      return (
        <html lang="en">
          <head>
            <title>React SSR Streaming Error Handle</title>
          </head>
          <body>
            <h1>Hello world!</h1>
          </body>
        </html>
      );
    };
    
    server.get("/ssr-error", (req: express.Request, res: express.Response) => {
      res.status(500);
      res.send("<p>SSR ERROR</p>");
    });
    
    server.get("/sample1", (req: express.Request, res: express.Response) => {
      const stream = renderToStaticNodeStream(<LargeApplication />);
      res.type("html");
      res.write("<!DOCTYPE html>");
      stream.pipe(res, { end: true });
      stream.on("error", (error) => {
        // res.redirect("/ssr-error"); // エラーハンドリング中にredirectをしても ERR_HTTP_HEADERS_SENT が出る
        res.status(500);
        res.setHeader(
          "Custom-Error-Code",
          "REACT:RENDER_TO_STATIC_NODE_STREAM_ERROR"
        );
        res.write(`<pre><code>${error.stack}</code></pre>`);
        res.end();
      });
    });
    
    server.get("/sample2", (req: express.Request, res: express.Response) => {
      try {
        res.type("html");
        res.write("<!DOCTYPE html>"); // can be immediately responded to
        const html = renderToStaticMarkup(<LargeApplication />);
        /* <---- I know that describing it here won't cause any problems. */
        // res.type("html");
        // res.write("<!DOCTYPE html>");
        /* ----> */
        res.write(html);
      } catch (error) {
        res.status(500);
        res.setHeader(
          "Custom-Error-Code",
          "REACT:RENDER_TO_STATIC_NODE_STREAM_ERROR"
        );
        res.send(`<pre><code>${error.stack}</code></pre>`);
      }
    });
    
    server.listen(SERVER_PORT, () => {
      console.log(`Serve start: http://localhost:${SERVER_PORT}`);
    });
    
[/code]

このサーバーは次のようなURLになっています。

  * **renderToStaticNodeStream** : <http://localhost:9000/sample1>
  * **renderToStaticMarkup** : <http://localhost:9000/sample2>

## 実行結果

`/sample1`にアクセスすると以下のエラーが出力され、サーバーが停止します。

[code]

    Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
        at ServerResponse.setHeader (_http_outgoing.js:485:11)react-ssr-error-handle/src/server.tsx:33:9)
        at ReactMarkupReadableStream.emit (events.js:210:5)
        at ReactMarkupReadableStream.EventEmitter.emit (domain.js:475:20)
        at emitErrorNT (internal/streams/destroy.js:92:8)
        at emitErrorAndCloseNT (internal/streams/destroy.js:60:3)
        at processTicksAndRejections (internal/process/task_queues.js:80:21)
    error Command failed with exit code 1.
    
[/code]

理由は明らかで、キャッチしたエラーに基づいて送信する`res.setHeader`が`res.write("<!DOCTYPE
html>")`より後にあるため、レスポンスヘッダーがレスポンスボディよりも後に来ているためです。

`/sample2`も同様の理由でエラーが投げられ、サーバーが終了します。

## 考えられる解決策

解決策は単純です。

  1. `renderToStaticMarkup`を利用して、完全なHTMLが出力されてからクライアントにレスポンスを返す。
  2. SSR時にエラーを投げない完全な実装をする（エラーバウンダリーの利用など）
  3. クライアントに常にステータスコード200を返し、SSRに失敗したところだけレンダリングしない

現実的な実装は **1** であり、この実装方法を現在は採用しています。  
**3** はGoogle検索にエラー時の状態がインデックスされる懸念があります（未検証）。

## 複雑なアプリケーションの場合はどうするか

ここで示したサンプルコードは比較的小さなアプリケーションです。そのためSSR時にエラーを投げない完全な実装を目指すことが可能です。しかしながら、実際には、アプリケーションは多くのライブラリに依存し、多岐にわたる条件分岐を持つため完全な実装を目指すことは困難です。また、フロントエンドアプリケーションは高速なレンダリングが要求されるため、開発者はユーザーにより早く結果を返す実装を行いたいと考えます。例えば、TTFBを短くするなど。

他に考えられるエラーハンドリングの案はありますか？

## 追記 2020/06/02 13:15

コンポーネントを分割し最初の表示に必要な分だけSSRしたとしても、同様にSSR時にエラーを投げると本質問のような問題に直面します。したがって、アプリケーションの大小の問題に関する記述を文中から削除しました。


CC BY-SA 4.0
2020-06-02T00:49:30.073
67204
2020-06-02T05:00:33.513
2020-06-02T05:00:33.513
7997
7997
question
node.js reactjs
SPAにおいてレスポンスパフォーマンスを損なわずにSSRのエラーハンドリングをする方法を模索しています


私だったらということですが、  
SPAでSSRといってもそれほど大きなページを一度に持ってくるということは難しいと思います。  
必要な箇所だけ必要な分だけ都度都度Ajax等でサーバ問い合わせを実施するというのが現実的ではないでしょうか？

たとえばGoogleMapなんかも、シングルページで構成されていますが、すべてのページを持ってきているのではなくて、画面を移動すると読み込み待ちが発生するように、画面の大きさには限界があり大きなページを読み込むのではなく、必要な分だけ裏側でサーバサイドに問い合わせを実施して、読み込んでレンダリングしています。

都度サーバに問い合わせするということであれば、  
エラーが発生した場合に、その都度で対処すればよくなります。

追記）  
質問からアプリケーションの大小が消えてしまったので、さらに追加で回答。  
個別でSSRをしてエラーとなった場合でかつ、致命的な500番を出してページを止めなければいけない場合のエラーだった場合は、500出力用ページを用意して、そちらにリダイレクトさせる方法をとるでしょう。


CC BY-SA 4.0
2020-06-02T01:53:24.200
67210
2020-06-02T04:24:52.483
2020-06-02T04:24:52.483
22665
22665
67204
answer