我在一名用户报告后自行发现并解决了此问题。我使用 AI 生成了 bug 报告,因为我比较懒,而且它做得比我好——在这个过程中,它还发现了一个核心 bug,我随后进行了手动验证。
虽然问题不大,但检查这种模式是否在其他地方也导致了问题可能是有意义的。
一切始于有人勾选了 enable_direct_s3_uploads,但实际上并未配置和启用 S3,这导致所有上传功能失效,后台返回 500 错误。我认为这个 500 错误值得深入调查。
摘要
当 enable_direct_s3_uploads 已启用,但当前活动的存储后端为本地存储时,尝试上传可能会引发以下错误:
NoMethodError (undefined method 'signed_request_for_temporary_upload' for an instance of FileStore::LocalStore)
app/services/external_upload_manager.rb:34:in 'ExternalUploadManager.create_direct_upload'
lib/external_upload_helpers.rb:56:in 'ExternalUploadHelpers#generate_presigned_put'
app/controllers/application_controller.rb:443:in 'block in ApplicationController#with_resolved_locale'
app/controllers/application_controller.rb:443:in 'ApplicationController#with_resolved_locale'
这似乎是由无效的设置状态与被覆盖的 before_action 回调组合导致的。
配置
有问题的状态实际上是:
SiteSetting.enable_direct_s3_uploads == true
SiteSetting.enable_s3_uploads == false
Discourse.store.is_a?(FileStore::LocalStore) == true
对于本地存储,Discourse.store 没有实现:
signed_request_for_temporary_upload
这是符合预期的,因为该操作仅对外部/S3 存储有意义。
预期行为
如果直接 S3 上传已启用,但当前存储后端为本地存储,请求应由 external_store_check 干净地拒绝。
理想情况下,还应防止或验证这种无效的网站设置组合。
实际行为
请求到达了 ExternalUploadManager.create_direct_upload,该方法在 FileStore::LocalStore 实例上调用了:
store.signed_request_for_temporary_upload(...)
从而导致 NoMethodError。
可能的原因
ExternalUploadHelpers 注册了以下回调:
before_action :external_store_check,
only: %i[
generate_presigned_put
complete_external_upload
create_multipart
batch_presign_multipart_parts
complete_multipart
abort_multipart
]
当存储后端为本地时,这应该能阻止请求到达外部上传代码。
然而,UploadsController 随后使用相同的过滤方法注册了另一个回调:
before_action :external_store_check,
only: %i[_show_secure_deprecated show_secure]
Rails 将重复注册相同的回调视为重新定义,因此后者似乎替换了早期的 only: 条件。

结果是,external_store_check 不再针对 generate_presigned_put 运行,导致本地存储进入了直接上传的代码路径。
复现步骤
- 配置一个使用本地上传存储的 Discourse 实例。
- 确保:
SiteSetting.enable_s3_uploads = false
SiteSetting.enable_direct_s3_uploads = true
- 尝试上传文件。
- 观察来自
FileStore::LocalStore的NoMethodError。
可以通过控制台检查状态:
[
SiteSetting.enable_direct_s3_uploads,
SiteSetting.enable_s3_uploads,
Discourse.store.class,
Discourse.store.external?
]
建议的修复方案
回调注册不应相互覆盖。
例如,将安全上传操作包含在现有的 external_store_check 回调中:
before_action :external_store_check,
only: %i[
generate_presigned_put
complete_external_upload
create_multipart
batch_presign_multipart_parts
complete_multipart
abort_multipart
_show_secure_deprecated
show_secure
]
或者,为安全上传操作使用单独的回调方法。
此外,增加验证以防止在 S3/外部上传禁用的情况下启用 enable_direct_s3_uploads 也是值得考虑的。
临时解决方案
对于使用本地上传存储的网站:
SiteSetting.enable_direct_s3_uploads = false
可以防止此错误。