はじめに
前回は疲れ果てて参加できなかったので、約2週間ぶりの参加となりましたが、Sendagaya.rb#313に参加させていただきました。
今回は自分がWebSocketにおけるエラーの処理の仕方が分からなかったため、相談させていただきました。
その内容をまとめたいと思います。
WebSocketにおけるエラー処理が分からない
悩んでいたところ
今、FJORD BOOT CAMPのスクラム開発のプラクティスで、bootcampのアプリに分報機能を実装するということに取り組んでおります。
実際のissueです↓
実際のPRです↓
これはAction CableとVue.jsを使って実装を試みているのですが、WebSocket通信におけるエラー処理が分からずに悩んでおりました。
具体的には、app/channels/timelines_channel.rb
がchannelにおけるコントローラーの役割を担っており、分報のCRUDやchannelがsubscribeされた時に、過去の分報一覧をフロント側に送るということを行っています。
しかし、このCRUD等が失敗した時、どのような処理をさせれば良いのかというところに悩んでおりました(これをエラー処理といって良いのか分かりませんが💦)。
自分なりの考え
上記の問題の解決方法として、以下のような2種類の方法を考えました。
- HTTP通信のステータスコードを送って、その内容によって、フロントエンド側で場合分けして処理させる方法。
例えば、分報の投稿を行うapp/channels/timelines_channel.rb
内で、broadcastを行うときにステータスコードを送ります。
def create_timeline(data) @timeline = Timeline.new(user_id: current_user.id, description: data["description"]) if @timeline.save ActionCable.server.broadcast "timelines_channel", { event: "create_timeline", timeline: @timeline, code: 200 } else ActionCable.server.broadcast "timelines_channel", { event: "create_timeline", timeline: nil, code: 422 } end end
そして、フロントエンド側は受け取ったステータスコードにより、処理を場合わけしていくというものです。
この方法は以下の記事に記載されておりました。
ruby on rails - ActionCable - Respond With Error - Stack Overflow
- HTTPのステータスコードは使わず、ブロードキャストしている
event
でerror
を送る。
1の方法を最初に見つけた時は、なるほどと思いましたが、そもそもWebSocket通信とHTTP通信は異なる通信規格なので、HTTP通信の方法を使ってもよいのかという懸念がありました。
そこで、以下のような方法を考えました。
def create_timeline(data) @timeline = Timeline.new(user_id: current_user.id, description: data["description"]) if @timeline.save ActionCable.server.broadcast "timelines_channel", { event: "create_timeline", timeline: @timeline } else ActionCable.server.broadcast "timelines_channel", { event: "error", timeline: nil } end end
このように{ event: "error" }
をbroadcastしてやる方法です。
フロントエンド側では、場合分けしてeventがerrorのときにエラー表示をするなどを処理を行わせます。
WebSocket通信は、触り始めて間もないので、どちらが自然な方法なのかどうかが分からなかったため、今回相談させていただきました。
頂いた意見
WebSocketの仕様書を読んでも、ハンドシェイクの時(WebSocket通信は最初にHTTPコネクションを繋いだ後に、 WebSocketコネクション上で双方向通信を行いますが、最初のHTTPコネクションを繋ぐところをハンドシェイクと言います)にはHTTPのステータスコードを使うとありますが、WebSocketコネクション上で、どのようにエラーに対処していけば良いのかがありませんでした(私が見逃しているだけかもしれません💦)
そもそも、HTTP通信ではリクエストとレスポンスが1対1対応しているからこそ、リクエストに対してステータスコードを返すわけですが、WebSocket通信は双方向通信であるため、この考え方自体が当てはまりません。
そして、仕様書を少し読んだ限りでは、上記のエラー処理については共通認識されている方法がなさそうでした。
つまり、エラーについて独自のルールを決めて、一貫性を持ってエラー処理を書いていかなければなりませんが、その時の考え方として、開発者達が分かりやすいかどうかという考え方があるそうです。
その観点で考えると、HTTPのステータスコードを便宜上使うのも、ありだという意見をいただきました。
また、eventを"cannot_create_timeline"、"cannot_delete_timeline"などと、eventをより詳しく分類して、eventで管理するという方法もあるという意見をいただきました。
私自身、後者の方が分かりやすいと考え、後者の方法で実装することにしました。
実際に実装したコード
上記のような意見を頂いて、自分なりに実装したコードが以下になります。
以下のようにsubscribeに失敗した時と、create、update、deleteに失敗したときに、
{ event: "failed_to_create_timeline" }
のようなeventをフロントエンド側に送ります。
app/channels/timelines_channel.rb
class TimelinesChannel < ApplicationCable::Channel before_subscribe :set_host_for_disk_storage def subscribed stream_from "timelines_channel" if !subscription_rejected? transmit({ event: "subscribe", current_user: decorated(current_user).format_user_to_channel, timelines: formatted_timelines }) else transmit({ event: "failed_to_subscribe" }) end end def create_timeline(data) set_host_for_disk_storage @timeline = Timeline.new(user_id: current_user.id, description: data["description"]) if @timeline.save broadcast_to_timelines_channel("create_timeline", @timeline) else broadcast_to_timelines_channel("failed_to_create_timeline", nil) end end def update_timeline(data) set_host_for_disk_storage @timeline = Timeline.find_by(id: data["id"]) if @timeline.update(description: data["description"]) broadcast_to_timelines_channel("update_timeline", @timeline) else broadcast_to_timelines_channel("failed_to_update_timeline", nil) end end def delete_timeline(data) set_host_for_disk_storage @timeline = Timeline.find_by(id: data["id"]) if @timeline.destroy broadcast_to_timelines_channel("delete_timeline", @timeline) else broadcast_to_timelines_channel("failed_to_delete_timeline", nil) end end end
そして、フロントエンド側のchannelでデータを受け取ったら、eventに応じて処理を変えています。CRUDが失敗した時は、コンソール上でメッセージを出すようにしました。
app/javascript/channels/timelines-channel.vue
this.timelinesChannel = this.$cable.subscriptions.create("TimelinesChannel", { received: (data) => { switch (data.event) { case 'subscribe': data.timelines.forEach((timeline) => { this.timelines.unshift(timeline) }) break case 'failed_to_subscribe': console.warn('Failed to subscribe') break case 'create_timeline': this.timelines.unshift(data.timeline) this.description = '' break case 'failed_to_create_timeline': console.warn('Failed to create timeline') break case 'update_timeline': this.timelines.forEach((timeline) => { if (timeline.id === data.timeline.id) { timeline.description = data.timeline.description } }) break case 'failed_to_update_timeline': console.warn('Failed to update timeline') break case 'delete_timeline': this.timelines.forEach((timeline, i) => { if (timeline.id === data.timeline.id) { this.timelines.splice(i, 1) } }) break case 'failed_to_delete_timeline': console.warn('Failed to delete timeline') break } } }) },
最後に
Sendagaya.rbに参加し、悩んでいるところを相談させていただき、頂いた意見を元に自分なりに実装をしたというお話でした。
Action CableやWebSocketに触り初めて2ヶ月弱ですが、自分の実力がないこともあり、ハマり所が多く、四苦八苦しておりましたが、上記の問題に目処がたったことでなんとか、もう少しでPRを出せるところまで行けると思います。
自分の直面した問題について、強いプログラマーの方々からご意見をいただけるSendagaya.rbは、とても有り難いです🙇♂️
上記の問題について、ご意見やご指摘などをいただけるととても有り難いです🙇♂️