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ミドルウェアは以下のいずれかに該当するときに圧縮を行いません。
- filter関数がfalseを返したとき
- shouldTransform関数がfalseを返したとき
- Content-Lengthが閾値(デフォルト: 1024)未満のとき
- 既に何かしらの圧縮が施されていたとき
- リクエストメソッドがHEADのとき
- Accept-Encodingヘッダーにidentityが含まれているとき(実際は重みに依る)
となっているので、 Cache-Control: no-transform
とすると、2番目の条件に該当して圧縮が行われない、というわけです。
もう一歩踏み込んだ対処
Cache-Contro: no-transform
が妥当なケースならそれで良いはずです。
ただ、 Cache-Control: no-transform
はCache-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を使え!