I found and resolved this issue myself after a client reported it. I used AI to generate the bug report because I was lazy and it does a better job than I do - and it found a core bug while doing that, which I manually verified.
Nothing big but it might make sense to see if this pattern is causing issues in other places as well.
It all started with someone having checked enable_direct_s3_uploads without actually configuring and enabling S3, which broke all uploads, returning 500 errors behind the scenes. I thought the 500 was worth investigating.
Summary
When enable_direct_s3_uploads is enabled while the active upload store is local, attempting an upload can raise:
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'
This appears to be caused by a combination of an invalid settings state and an overwritten before_action callback.
Configuration
The problematic state is effectively:
SiteSetting.enable_direct_s3_uploads == true
SiteSetting.enable_s3_uploads == false
Discourse.store.is_a?(FileStore::LocalStore) == true
For local storage, Discourse.store does not implement:
signed_request_for_temporary_upload
which is expected, since that operation only makes sense for an external/S3 store.
Expected behaviour
If direct S3 uploads are enabled while the active store is local, the request should be rejected cleanly by external_store_check.
Ideally the invalid site setting combination should also be prevented or validated.
Actual behaviour
The request reaches ExternalUploadManager.create_direct_upload, which calls:
store.signed_request_for_temporary_upload(...)
on a FileStore::LocalStore, resulting in a NoMethodError.
Possible cause
ExternalUploadHelpers registers the following callback:
before_action :external_store_check,
only: %i[
generate_presigned_put
complete_external_upload
create_multipart
batch_presign_multipart_parts
complete_multipart
abort_multipart
]
This should prevent the request from reaching the external upload code when the store is local.
However, UploadsController later registers another callback using the same filter method:
before_action :external_store_check,
only: %i[_show_secure_deprecated show_secure]
Rails treats repeated registration of the same callback as a redefinition, so the latter appears to replace the earlier only: conditions.

As a result, external_store_check is no longer run for generate_presigned_put, allowing a local store to reach the direct-upload code path.
Reproduction
- Configure a Discourse instance with local upload storage.
- Ensure:
SiteSetting.enable_s3_uploads = false
SiteSetting.enable_direct_s3_uploads = true
- Attempt to upload a file.
- Observe the
NoMethodErrorfromFileStore::LocalStore.
A console check of the state can be done with:
[
SiteSetting.enable_direct_s3_uploads,
SiteSetting.enable_s3_uploads,
Discourse.store.class,
Discourse.store.external?
]
Suggested fix
The callback registrations should not overwrite each other.
For example, include the secure upload actions in the existing external_store_check callback:
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
]
Alternatively, use a separate callback method for the secure-upload actions.
It may also be worth adding validation so that enable_direct_s3_uploads cannot be enabled while S3/external uploads are disabled.
Workaround
For sites using local upload storage:
SiteSetting.enable_direct_s3_uploads = false
prevents the error.