koheitakahashiのブログ

2020.07.01にプログラマーとして生を受けた私が学んだことや、日常について徒然に書いていきます。

ActionController::Liveを使っていてハマったところ

概要

この記事の内容を大まかにまとめると以下になります。

ActionController::Liveを使っていてstream.closeされたら、cookieの値を変更するという処理を実装しようとしたのですが、cookieの値が変更できないという問題に直面しました。

そもそも、今回はチャンク形式転送エンコーディングという方式でレスポンスを返していたのですが、その仕組みは最初にクライアントにレスポンスを送って、そのレスポンスボディに逐次データを書き込んでいるというものでした。

そのため、レスポンスを最初に送っているため、レスポンスヘッダーが変更不可能な状態になります。よって、データを送りはじめたら、それ以降同一レスポンス内ではcookieは変更できないということでした。

環境

  • Rails 6.0.3

本題

実装しようとしていたこと

フロントエンドがVue.js、バックエンドがRailsと別れている中で、ファイルダウンロードの仕組みに追加実装しようとしていました。
そのファイルダウンロードの仕組みは、ActionController::Liveを使用して、ある程度のデータの塊に分割してクライアントにデータを送るというものでした。(具体的には以下のようなコードになります)

class FileDownloadsController < ApplicationController
  include ActionController::Live
  
  def create
    file = fetch_file # fetch_fileはファイルをとってくる処理
    
    # 100KBごとにクライアントにデータを送る
    while (chunk = file.read(100000).presence)
      response.stream.write chunk
    end
    
    response.stream.close
  end
end

上記のコードがある中で、今回私が実装しようとしたものは、ダウンロードの完了を検知して画面の表示を変えるというものでした。

当初考えた実装方針は以下のようなものになります。

  1. フロントエンド側は、ファイルダウンロードボタンを押された後から、1秒に1回ほどの頻度でcookieを監視する

  2. FileDownloadsController#createで、ダウンロードが完了したら、cookieにdownloading=0などの値を入れる

  3. フロントエンドは、downloading=0になったことを検知できたら画面の表示を変更させて、cookieの監視をやめる

コードレベルでは以下のようなイメージです。

class FileDownloadsController < ApplicationController
  include ActionController::Live

  def create
    cookie[:downloading] = 1 # ここを追加
    file = fetch_file # fetch_fileはファイルをとってくる処理

    # 100KBごとにクライアントにデータを送る
    while (chunk = file.read(100000).presence)
      response.stream.write chunk
    end

    response.stream.close
    cookie[:downloading] = 0 # ここを追加
  end
end

ハマったこと

上記のようなコードを実装して、フロントエンド側でcookieを監視してdownloading = 1となっていることは確認できました。
しかし、ダウンロードを完了してもdownloading = 0には変更されていないという問題がありました。

なぜ上記の方法で実現できなかったのか

そもそも、どうやって分割してデータが送られているのかという大枠を知らなければなりませんでした。
具体的にはチャンク形式転送エンコーディングという方法がHTTPレイヤーでは使われています。

Transfer-Encoding - HTTP | MDN に記載がありますが、チャンク化転送エンコーディングというのは、データを分割してそれを逐次送っていくという仕組みです。

具体的には、最初にレスポンスヘッダーとレスポンスボディの最初のチャンクを送って、それ以降は\r\n区切りで分割したデータがレスポンスボディに逐次書き込まれていきます。

このような仕組みにより、ファイルの分割ダウンロードが実現されていました。

ここで、最初にレスポンスヘッダーをクライアントに送っているという都合上、レスポンスヘッダーは後から変更不可能な状態になります。
ゆえに、ダウンロードが完了した瞬間にcookieを変更することができないということでした。

感想

なぜcookieが書き換えられないのだと数時間悩んでいたのですが、HTTPレイヤーではどのような仕組みが採用されているのかに関心を向けられれば、原因が分かるのが早かったと思いました。

「ActionController::Live cookie」などで検索しても、中々情報がヒットしなかったので、同じような問題に直面した方の参考になれば嬉しいです。

参考