Rails 7中ActionText/Trix图片上传失败问题求助
问题描述
在Rails 7项目中使用ActionText/Trix为博客上传图片时,无论配置本地磁盘存储还是AWS S3,都会出现相同问题:纯文本编辑功能正常,但点击回形针图标上传图片时,选择文件后前端控制台立即报错422,提交表单后图片未保存,仅收到博客更新成功的提示。
前端控制台错误
POST http://localhost:3000/rails/active_storage/direct_uploads 422 (Unprocessable Entity) actiontext.js:543
Uncaught Error: Direct upload failed: Error creating Blob for "Screenshot from 2022-10-13 12-38-57.png". Status: 422
at AttachmentUpload.directUploadDidComplete (actiontext.js:849:13)
at BlobRecord2.callback (actiontext.js:618:13)
at BlobRecord2.requestDidError (actiontext.js:560:12)
at BlobRecord2.requestDidLoad (actiontext.js:556:14)
at XMLHttpRequest. (actiontext.js:527:56)
Rails服务器端错误
Started POST "/rails/active_storage/direct_uploads" for ::1 at 2022-11-03 14:20:10 -0500
Processing by ActiveStorage::DirectUploadsController#create as JSON
Parameters: {"blob"=>{"filename"=>"Screenshot from 2022-10-26 15-49-39.png", "content_type"=>"image/png", "byte_size"=>336871, "checksum"=>"TZneH7Z7DdCSftEvona6zg=="}, "direct_upload"=>{"blob"=>{"filename"=>"Screenshot from 2022-10-26 15-49-39.png", "content_type"=>"image/png", "byte_size"=>336871, "checksum"=>"TZneH7Z7DdCSftEvona6zg=="}}}
Completed 422 Unprocessable Entity in 33ms (ActiveRecord: 8.4ms | Allocations: 14851)
ActiveRecord::RecordInvalid (Validation failed: Service name can't be blank):
已尝试的排查步骤
- 禁用博客页面的Turbo功能,无效
- 多次查阅Active Storage和ActionText官方文档,未找到对应解决方案
- 确认
storage.yml中已配置本地存储服务
相关配置与代码
storage.yml
local: service: Disk root: <%= Rails.root.join("storage") %>
表单代码(app/views/blogs/_form.html.erb)
<div class="form-group"> <%= form.rich_text_area :body, class: 'form-control', rows: 15, placeholder: 'Content' %> </div>
控制器代码(app/controllers/blogs_controller.rb)
class BlogsController < CommentsController before_action :set_blog, only: %i[ show edit update destroy toggle_status ] before_action :set_sidebar_topics, except: [:update, :create, :destroy, :toggle_status] layout "blog" access all: [:show, :index], user: { except: [:destroy, :new, :create, :update, :edit, :toggle_status] }, admin: :all, testing: { except: [:destroy, :create, :update]} def index if logged_in?(:admin) || logged_in?(:testing) @blogs = Blog.recent.with_rich_text_body_and_embeds.page(params[:page]).per(5) else @blogs = Blog.published.recent.with_rich_text_body_and_embeds.page(params[:page]).per(5) end @page_title = "My Portfolio Blog" end def show if logged_in?(:admin) || logged_in?(:testing) || @blog.published? @blog = Blog.includes(:comments).friendly.find(params[:id]) @comment = Comment.new @page_title = @blog.title @seo_keywords = @blog.body else redirect_to blogs_path, notice: 'You are not authorized to access this page.' end end def new @blog = Blog.new end def edit end def create @blog = Blog.new(blog_params) respond_to do |format| if @blog.save format.html { redirect_to blog_url(@blog), success: "Blog was successfully created." } else flash[:danger] = "Blog must have title, body and topic." format.html { render :new, status: :unprocessable_entity } end end end def update respond_to do |format| if @blog.update(blog_params) format.html { redirect_to blog_url(@blog), success: "Blog was successfully updated." } else format.html { render :edit, status: :unprocessable_entity } end end end def destroy @blog.destroy respond_to do |format| format.html { redirect_to blogs_url, status: :see_other } end end def toggle_status if @blog.draft? && logged_in?(:admin) @blog.published! elsif @blog.published? && logged_in?(:admin) @blog.draft! end redirect_to blogs_url, success: 'Post status has been updated.' end private def set_blog @blog = Blog.friendly.find(params[:id]) end def blog_params params.require(:blog).permit(:title, :body, :topic_id, :status, images: []) end def set_sidebar_topics @side_bar_topics = Topic.with_blogs end end
模型代码(app/models/blog.rb)
class Blog < ApplicationRecord enum status: { draft: 0, published: 1 } extend FriendlyId friendly_id :title, use: :slugged validates_presence_of :title, :body, :topic_id has_rich_text :body has_many_attached :images, dependent: :destroy has_many :comments, as: :commentable, dependent: :destroy, counter_cache: :commentable_count belongs_to :topic, optional: true def self.special_blogs all end def self.featured_blogs limit(2) end def self.recent order("updated_at DESC") end end
数据库Schema
ActiveRecord::Schema[7.0].define(version: 2022_10_14_225319) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "action_text_rich_texts", force: :cascade do |t| t.string "name", null: false t.text "body" t.string "record_type", null: false t.bigint "record_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true end create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false t.bigint "record_id", null: false t.bigint "blob_id", null: false t.datetime "created_at", null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end create_table "active_storage_blobs", force: :cascade do |t| t.string "key", null: false t.string "filename", null: false t.string "content_type" t.text "metadata" t.string "service_name", null: false t.bigint "byte_size", null: false t.string "checksum" t.datetime "created_at", null: false t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end create_table "active_storage_variant_records", force: :cascade do |t| t.bigint "blob_id", null: false t.string "variation_digest", null: false t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end create_table "blogs", force: :cascade do |t| t.string "title" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "slug" t.integer "status", default: 0 t.bigint "topic_id" t.integer "commentable_count" t.text "body" t.index ["slug"], name: "index_blogs_on_slug", unique: true t.index ["topic_id"], name: "index_blogs_on_topic_id" end create_table "comments", force: :cascade do |t| t.bigint "user_id", null: false t.string "commentable_type", null: false t.bigint "commentable_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.text "body" t.index ["commentable_type", "commentable_id"], name: "index_comments_on_commentable" t.index ["user_id"], name: "index_comments_on_user_id" end create_table "friendly_id_slugs", force: :cascade do |t| t.string "slug", null: false t.integer "sluggable_id", null: false t.string "sluggable_type", limit: 50 t.string "scope" t.datetime "created_at" t.index ["slug", "sluggable_type", "scope"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope", unique: true t.index ["slug", "sluggable_type"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type" t.index ["sluggable_type", "sluggable_id"], name: "index_friendly_id_slugs_on_sluggable_type_and_sluggable_id" end create_table "portfolios", force: :cascade do |t| t.string "title" t.string "subtitle" t.text "body" t.text "main_image" t.text "thumb_image" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "position" end create_table "skills", force: :cascade do |t| t.string "title" t.integer "percent_utilized" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.text "badge" end create_table "technologies", force: :cascade do |t| t.string "name" t.bigint "portfolio_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["portfolio_id"], name: "index_technologies_on_portfolio_id" end create_table "topics", force: :cascade do |t| t.string "title" t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "name" t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" t.integer "sign_in_count", default: 0, null: false t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" t.string "current_sign_in_ip" t.string "last_sign_in_ip" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "roles" t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "blogs", "topics" add_foreign_key "comments", "users" add_foreign_key "technologies", "portfolios" end
解决建议
1. 配置Active Storage默认服务
打开config/environments/development.rb,添加或确认以下配置:
config.active_storage.service = :local
缺少该配置时,Active Storage无法识别当前环境使用的存储服务,导致active_storage_blobs表的service_name字段为空,触发验证错误。
2. 重启Rails服务器
添加配置后,重启服务器使配置生效。
3. 验证修复效果
再次尝试上传图片,检查服务器日志是否仍有Service name can't be blank错误。若配置正确,图片将正常上传并保存到指定存储位置。
内容的提问来源于stack exchange,提问作者DebakeDSP94

