プレビュー不可の添付ファイルが壊れた画像として表示される問題を修正

basecamp/lexxy

Active Storageの添付ファイルで previewable="false" が正しく解釈されず壊れた画像として表示される問題と、パスワード保護されたPDFなどプレビュー生成に失敗するファイルでも同様の問題が発生していた状況を修正しました。

背景

DOMの getAttribute は属性値を常に文字列として返すため、previewable="false" は文字列 "false" として取得されます。JavaScriptでは空文字列でない文字列はすべてtruthyと評価されるため、"false"true として扱われてしまいます。この挙動により、本来ファイルアイコンとファイル名で表示されるべき非プレビュー対象の添付ファイルが、画像として描画を試みて壊れたimgタグを生成していました。

また、previewable="true" であっても、パスワード保護されたPDFのようにActive Storageのrepresentation URLが500エラーを返す場合、<img> タグのsrcが取得できず同様に壊れた画像が表示されるケースも存在していました。この問題は previewable フラグの解釈とは独立した別の障害パスです。

技術的な変更

この修正は2つの独立したアプローチで構成されています。1つ目は previewable 属性の型変換の正規化、2つ目は画像読み込み失敗時のフォールバック処理の追加です。

parseBoolean 関数の追加により、文字列 "false" をboolean false として正しく評価できるようになりました。src/helpers/string_helper.js に追加されたこの関数は、値が文字列の場合は === "true" の比較でboolean変換を行い、それ以外は Boolean() に委譲します。

export function parseBoolean(value) {
  if (typeof value === "string") return value === "true"
  return Boolean(value)
}

ActionTextAttachmentNode のコンストラクタでは、this.previewable = previewable だったのを this.previewable = parseBoolean(previewable) に変更し、DOMから読み込まれた文字列値が確実にboolean型に変換されるようになりました。

onerror フォールバック#createDOMForImage メソッドに追加されました。previewable がtrueかつ isPreviewableImage でない場合(PDFなどの非画像プレビュー対象)に限り、img.onerror ハンドラを設定します。

#createDOMForImage(options = {}) {
  const img = createElement("img", { src: this.src, draggable: false, alt: this.altText, ...this.#imageDimensions, ...options })

  if (this.previewable && !this.isPreviewableImage) {
    img.onerror = () => this.#swapPreviewToFileDOM(img)
  }

  const container = createElement("div", { className: "attachment__container" })
  container.appendChild(img)
  return container
}

#swapPreviewToFileDOM メソッドは、エラー発生時に figure 要素のCSSクラスを attachment--preview から attachment--file へ置き換え、preview用の attachment__containerfigcaption を削除した上で、ファイルアイコンとファイル名の表示要素を追加します。重要なのは、このDOM操作はあくまで表示上の変換であり、ノードの previewable プロパティ自体は true のまま保持される点です。これにより exportDOM がサーバー向けに元の previewable 状態を正しく保持できます。

#swapPreviewToFileDOM(img) {
  const figure = img.closest("figure.attachment")
  if (!figure) return

  figure.className = figure.className.replace("attachment--preview", "attachment--file")

  const container = figure.querySelector(".attachment__container")
  if (container) container.remove()

  const caption = figure.querySelector("figcaption")
  if (caption) caption.remove()

  figure.appendChild(this.#createDOMForFile())
  figure.appendChild(this.#createDOMForNotImage())
}

ブラウザテスト test/browser/tests/attachments/non_previewable_attachment.test.js が新たに追加され、previewable="false" でのファイル表示確認と、壊れたプレビューURLでの onerror フォールバック動作の両方が自動化されています。

設計判断

parseBoolean をヘルパー関数として共通化した判断は、同種の問題が他の属性でも発生しうることを踏まえた設計です。DOM属性の文字列→boolean変換は ActionTextAttachmentNode 固有の問題ではなく、汎用的なユーティリティとして string_helper.js に配置することで再利用可能にしています。

onerror フォールバックをノードのプロパティ変更なしにDOMレベルで処理する設計も注目に値します。previewable プロパティを false に書き換えると exportDOM の出力も変化してしまい、サーバー側の状態と不整合が生じます。DOM操作のみで見た目を変換することで、エディタ内部の状態とサーバー向けのシリアライズを切り離しています。

onerror ハンドラの適用条件 として !this.isPreviewableImage を設けている点も重要です。通常の画像ファイル(isPreviewableImagetrue)に対してはハンドラを設定しないことで、通常画像の読み込み失敗を不必要にファイル表示に変換しないよう区別しています。

まとめ

DOMのstring/boolean境界を parseBoolean で明示的に正規化し、onerror によるDOMレベルのフォールバックでサーバー状態と表示状態を分離するという2層の対策により、この修正は型の曖昧さと外部依存の失敗という異なる根本原因をそれぞれ独立して解決しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
87fe5235

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術的変更・設計判断(各論)、まとめ(結論)の3部構成が明確に適用されています。各セクションの役割がはっきりしており、読者が変更の全体像から詳細までスムーズに理解できる構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```javascript:filepath```)およびGitHubのPRリンク記法([#900](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

DOMの属性取得の挙動、JavaScriptのtruthyな値、onerrorイベントハンドラといった専門的な内容を扱っており、対象読者であるエンジニアに適した技術レベルと表現です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論から始まり、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されています。1段落1トピックが守られ、段落長も適切であるため、非常に可読性が高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されている`parseBoolean`関数、`ActionTextAttachmentNode`のコンストラクタ修正、`#createDOMForImage`メソッドへの`onerror`追加、`#swapPreviewToFileDOM`メソッドの追加は、すべて提供されたDiff情報と正確に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`getAttribute`、`truthy`、`onerror`、`DOM`、`figure`、`figcaption`といった技術用語が文脈に応じて正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「文字列`"false"`がtruthyと評価される」というJavaScriptの挙動や、`onerror`ハンドラによるフォールバック処理の説明は、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(`previewable="false"`の問題、500エラー時のフォールバック、ノードのプロパティを維持する設計など)は、PRのDescriptionおよびDiff内容によって完全に裏付けられています。ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号 `#900` が正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「プレビュー不可の添付ファイルが壊れた画像として表示される問題を修正」は、PRのタイトル「Fix non-previewable attachments rendered as broken images」の内容を正確に反映しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PRで言及されていないバージョン情報やリリース予定などの外部知識は含まれておらず、提供された情報源に忠実です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

完了した変更に対して「〜しました」「〜でした」といった過去形の表現が使われており、PRの状況と時間表現が一致しています。