READYFOR Tech Blog

READYFOR のエンジニアブログ

ActiveStorageを使った画像をメール内に表示しようとしたらエラーが起きまくった失敗談

こんにちは、READYFORプロダクトエンジニアの斉藤です。

これは READYFOR Advent Calendar 2022 の19日目の記事です。

はじめに

この記事はActiveStorageを使ってファイルアップロード機能を新規開発した際にSlackのエラーチャンネルを賑わせてしまった失敗談です。

開発について

READYFORのクラウドファンディングサービスの中で、プロジェクトを実施する実行者がお知らせ記事を投稿できる新着情報という機能があります。 これまで、この新着情報の一覧画面では本文中の画像がサムネイルとして表示される仕様になっていました。
今回、この新着情報にサムネイル画像を設定することで本文中の画像でなく設定した画像を表示できるようにするための新規開発を行いました。

ファイルアップロード機能のライブラリはActiveStorageを使いました。

弊社サービスではまだまだPaperclip(すでにdeprecationが発表されています)が使われている箇所がほとんどで、私自身ActiveStorageを使った開発は今回が初めてでした。
※社内でActiveStorage移行プロジェクトが別途進行中

エラー発生

リリースしてしばらく経ち、エラーなども発生しておらず安心していました。

ところがある日突然、嵐のようにSlackのエラーチャンネルにエラー通知が飛んできたのです。

ActiveRecord::RecordNotFound: Couldn't find ActiveStorage::Blob with 'id'=xxxx

ん?画像が見つからない?
何が起きているのでしょうか?

原因

調査を進めていくと、どうやらメールが怪しいことがわかりました。

新着情報が投稿されると、そのプロジェクトに支援しているユーザーやプロジェクトをウォッチリストに追加しているユーザーへ投稿通知メールを送信しているのですが、そのメールで起きていたのです。

上記メールの本文内に、今回の開発で設定できるようになったサムネイル画像のパス(実際はActiveStorageが生成するURL)を渡して表示していました。
そう、メール送信後にサムネイル画像を削除した場合にメールを開くとエラーが起きてしまっていたのです。

暫定対応

Slackのエラーチャンネルがエラー通知だらけになってしまい、確認すべき他のエラーが流れてしまう状況でした。
なのでとりあえずエラー通知を抑えるべく、上記エラーが発生したActiveStorage:BlobのIDを対象IDに追加して、エラー通知から除外するパッチをあてる対応をしました。 (毎日対象IDが増えていくので毎日IDを追加していく対応が必要になってしまいました。。対応してくださった皆さんありがとうございました)

module SuppressActiveStorageBlobRecordNotFoundError
  extend ActiveSupport::Concern

  TARGET_IDS = [
    xxxx, xxxx, xxxx, xxxx, ...
  ].freeze

  included do
    rescue_from ActiveRecord::RecordNotFound do |e|
      # `ActiveStorage::Blob` 以外の場合や未知のIDの場合は標準のハンドリングに任せる
      raise e if e.model != ActiveStorage::Blob.name || !e.id.in?(TARGET_IDS)

      Rails.logger.info "Suppress #{e.model} record not found error: id=#{e.id}"
      head :not_found
    end
  end
end

Rails.application.config.to_prepare do
  ActiveStorage::BlobsController.include(SuppressActiveStorageBlobRecordNotFoundError)
  ActiveStorage::RepresentationsController.include(SuppressActiveStorageBlobRecordNotFoundError)
end

恒久対応

画像ファイルのパスを渡さずに画像を表示するにはどうすればよいか?

まずは画像ファイルをバイナリにしてメール本文内で表示する方法を試みました。
ところがこれはメールクライアントによって表示されないことがあったため断念。(Gmailもだめでした)

それでは画像ファイルをインライン添付して本文中に表示するのはどうか。
これでうまくいきました!

Mailer側ではattachmentsに対してinlineを呼び出すだけ。
View側ではattachments に対してurlを呼び出すだけです。

# model
class Announcement < ApplicationRecord
  has_one_attached :thumbnail
end
# Mailer
def announcement(announcement_id)
  @announcement = Announcement.find(announcement_id)
  if @announcement.thumbnail.attached?
    thumbnail_file = @announcement.thumbnail.download
    @attachment_thumbnail_filename = @announcement.thumbnail.filename.to_s
    attachments.inline[@attachment_thumbnail_filename] = thumbnail_file
  end
  mail to: xxx, subject: xxx
end
# view
<% attachment_file = attachments[@attachment_thumbnail_filename] %>
<% if attachment_file.present? %>
  <%= image_tag attachments[@attachment_thumbnail_filename].url, alt: @announcement.title %>
<% end %>

参考: Action Mailer の基礎 - Railsガイド

さいごに

初歩的なミスではあったのですが、意外と盲点で開発時にもQA時にも気づくことができませんでした。
ファイル関連は意外なところに落とし穴があったりするので要注意だと肝に銘じた経験でした。