5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Reactのプロキシを通すとSSE (Server-sent events)のレスポンスを受け取れない

Last updated at Posted at 2020-03-05

React、というかreact-scriptsはプロキシ機能を備えています。

このプロキシ、基本的にはウェブAPIのために用います。React自体はlocalhost:3000で実行し、ウェブAPIはlocalhost:8080で実行する場合、localhost:3000にウェブAPIのリクエストを投げるとlocalhost:8080にプロキシする、という具合ですね1

使用方法も簡単で、 package.json に下記のようなフィールドを付け加えるだけで動作します。

{
  // ...
  "proxy": "http://localhost:8080",
  // ...
}

ただこのプロキシを通してSSE (Server-sent events)なウェブAPIにリクエストを投げると、レスポンスのヘッダーなどは受け取れるものの、本文が受け取れません。悲しいね……。

という事実を述べて終わりたいわけじゃなく、なぜそうなるのか、どうすれば解決できるのかまとめてみました。

手っ取り早く解決したい方へ

「理由はどうでもいいから一刻も早く解決したい!」という方はここ読むだけで大丈夫です。

Reactアプリケーションのソースコードが置いてあるルートに以下の内容を setupProxy.js というファイル名で保存してください。ちなみに/apiへのリクエストをlocalhost:8080にプロキシする、という内容ですので、その辺は適宜変更してください。

const proxy = require('http-proxy-middleware');

module.exports = (app) => {
  app.use('/api', proxy({
    target: 'http://localhost:8080',
    onProxyReq: (proxyReq, req) => {
      if (req.headers['accept'] === 'text/event-stream') {
        req.headers['accept-encoding'] = 'identity';
      }
    }
  }));
};

このファイルを置いた後、react-scripts restartを行えば解決すると思います。

原因

react-scriptsは内部でwebpack-dev-serverを利用しています。webpack-dev-serverはさらにExpress.jsを内包しており、そのミドルウェアで様々な機能を実現しています。プロキシもそのひとつです2

そして、デフォルトでレスポンスの圧縮も行っています3。圧縮もExpress.jsのミドルウェアで実現されており、compressionがその実体です。

で、原因はずばりこの圧縮にあります。この件はすでにOption to disable the compression for the webpack dev server · Issue #7847 · facebook/create-react-appなどで言及されています。

対処

さきほどのIssueにもコメントとして残されていますが、SSEなウェブAPIのレスポンスヘッダーに Cache-Control: no-transform を付与すればこの問題は解決します。

なぜ解決するかはcompressionミドルウェアのソースコードを眺めると簡単に分かります。

compressionミドルウェアは以下のいずれかに該当するときに圧縮を行いません

となっているので、 Cache-Control: no-transform とすると、2番目の条件に該当して圧縮が行われない、というわけです。

もう一歩踏み込んだ対処

Cache-Contro: no-transform が妥当なケースならそれで良いはずです。

ただ、 Cache-Control: no-transformCache-Control - HTTP | MDNでも触れられている通り、プロキシにおけるあらゆる変換の禁止を宣言するものです。それにより圧縮も行われなくなるのですが、圧縮は許容しないが、他の変換は許容するという場合には不適切です。

まーーーーーーーいろいろ考慮することはあると思うのですが、今回に限れば圧縮が悪さしているので、圧縮にのみアプローチできるのが最も影響範囲が小さいと思います。そうなると Cache-Control: no-transform はやや影響範囲が大きいかな……、と思う。ちょっと強引な気もするけど。SSEであれば実際はあらゆる変換を禁止するのが妥当な気もするけど。

とりあえず圧縮にのみアプローチしたい、となった場合、やはり着目すべきはAccept-Encodingヘッダーでしょう。実際compressionミドルウェアもAccept-Encodingヘッダーにidentityが含まれていれば圧縮を行わないので、対処自体は妥当だと思います。

というわけでAccept-Encodingヘッダーを付けよう!と思ったけど、SSEのクライアントとして使うであろうEventSourceはリクエストヘッダーを指定できない!ていうかそもそもAccept-Encodingヘッダーはプログラムによる指定を禁止されている

となるので、先にも挙げたこのコードになるのです。

const proxy = require('http-proxy-middleware');

module.exports = (app) => {
  app.use('/api', proxy({
    target: 'http://localhost:8080',
    onProxyReq: (proxyReq, req) => {
      if (req.headers['accept'] === 'text/event-stream') {
        req.headers['accept-encoding'] = 'identity';
      }
    }
  }));
};

react-scriptsはsetupProxy.jsを読み込むようになっており、そこで詳細なプロキシの設定を行えます

そのとき、http-proxy-middleware(実際は内包しているhttp-proxy)にてプロキシ時のイベントを捉えることが出来ます。そこで、EventSourceからのリクエスト、つまりAccept: text/event-streamのときに限りAccept-Encoding: identityとしてあげます。

するとcompressionミドルウェアはそれを考慮し、圧縮を行いません。

すごくめでたしな感じしません?な?なーーー!?とりあえず私はこうしています。

また別の対処

CORSを使え!

  1. もちろんCORSを利用して直接呼び出すのもあり:ant:

  2. https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/scripts/start.js#L123-L135

  3. https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/config/webpackDevServer.config.js#L45-L46

  4. 圧縮可能かはcompressibleに委譲している

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?