Deprecation warning about ::UploadIO constant when used without faraday-multipart gem
jimeh opened this issue ยท 9 comments
Basic Version Info
Faraday Version: 2.8.1
Ruby Version: 3.2.2
Issue description
If your project has the multipart-post
gem loaded, and uses faraday-retry
for POST requests without the faraday-multipart
gem, the Faraday::Retry::Middleware#rewind_files
method triggers a deprecation warning:
.../faraday-retry-2.2.0/lib/faraday/retry/middleware.rb:216: warning: constant ::UploadIO is deprecated
We specifically noticed this via the acme-client
gem, which performs post requests with retries enabled.
Most people may not notice this however, as it requires you to set Warning[:deprecated] = true
for Ruby to emit these deprecation warnings. We've only discovered due to our Ruby 2 -> 3 and Rails 6 -> 7 upgrades, as we've enabled notifications for all deprecation warnings during this process.
Actual behavior
This is because the Middleware class is referring to the Faraday::UploadIO
constant from the faraday-multipart
gem. If that gem is not part of the dependency tree however, it will instead treat it as ::UploadIO
, which is marked as a deprecated constant here in the multipart-post
gem.
Expected behavior
No deprecation warning is triggered.
Also, faraday-retry
probably should not rely on / expect a constant from another gem (faraday-multipart
) which it does not depend upon.
Steps to reproduce
# frozen_string_literal: true
Warning[:deprecated] = true
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'faraday'
gem 'faraday-retry'
gem 'multipart-post'
end
count = 0
expected = 5
retry_options = {
max: expected,
interval: 0.1,
methods: [:post],
retry_statuses: [503],
retry_block: proc { count += 1 }
}
conn = Faraday.new do |conn|
conn.request :retry, **retry_options
conn.adapter :test do |stub|
stub.post '/fail' do
[503, { 'Content-Type' => 'text/plain' }, ['Service Unavailable']]
end
end
end
conn.post('/fail', payload: { 'name' => 'John Doe' })
exit 0 if count == expected
warn "Retried #{count} times, expected #{expected}"
exit 1
Right, the code in there is actually using the UploadIO
constant, to check whether something in there is rewindable.
def rewind_files(body)
return unless defined?(UploadIO)
return unless body.is_a?(Hash)
body.each do |_, value|
value.rewind if value.is_a?(UploadIO)
end
end
EDIT: I filed lostisland/faraday-multipart#12 which is about getting around this deprecation warning by releasing a new major version of faraday-multipart which does not refer to old aliases.
I remember there was a fix in faraday-multipart
around the issue with a change in multipart-post
classes, the deprecation warning should originate from that.
This gem does not directly depend on multipart-post
or faraday-multipart
but has an "optional integration" that kicks in if multipart files are detected.
I believe the fix here would be to replace UploadIO
with Faraday::UploadIO
, since the latter will automatically point to the correct constant based on the multipart-post
version in use.
The side-effect of this change though it that the rewind will only work if you also have the faraday-multipart
gem loaded, as otherwise Faraday::UploadIO
won't be defined.
However I believe that's a fair assumption to make, as this was probably the original intention behind the integration.
@olleolleolle what do you think?
@iMacTia Yes, following that assumption is good.
To make things slow, long and predictable, we may want to do it in a few phases.
- We can release something which emits deprecation warnings with steps to take "depend on this gem, too".
- We can then release something which removes support.
- We can then release lostisland/faraday-multipart#12 - which removes the old constant aliases.
That looks like a sound plan, and will definitely avoid surprises for anyone who is somehow relying on UploadIO payload without the faraday-multipart
gem. I don't really think that's a thing, but have no way to confirm this hunch so better safe than sorry!
I don't think that the faraday-multipart
gem actually has anything to do with this. As the rewind
method triggers the deprecation warning when said gem is not present.
Since the #rewind
method is specifically looking for a UploadIO
-based object, maybe a simpler solution is to just decouple from Faraday::UploadIO
defined in faraday-multipart
, and just handle the different constants directly?
Something like this:
def upload_io_const
@upload_io_const ||= if defined?(::Multipart::Post::UploadIO)
::Multipart::Post::UploadIO
elsif defined?(::UploadIO)
::UploadIO
end
end
def rewind_files(body)
return unless upload_io_const
return unless body.is_a?(Hash)
body.each_value do |value|
value.rewind if value.is_a?(upload_io_const)
end
end
Alternatively, the implementation from faraday-multipart
could just be duplicated into faraday-retry
, though maybe as Faraday::Retry::UploadIO
to keep it internal.
Yeah so I was trying to avoid replicating the same logic in faraday-retry
and instead suggesting we reuse the one we have in faraday-multipart
. That would cause the "dependency" (still, a soft one because we check with defined?
first) with faraday-multipart
, which we think would be OK.
Basically if you want faraday-retry
to work automatically with file uploads, you'd need to also use faraday-multipart
.
I haven't used the latter in a while, so I'm unsure if you can actually pass an UploadIO
object to a Faraday request and get that correctly sent without having faraday-multipart
in your stack. If that's not possible anyway, then the soft dependency looks like a reasonable solution
OK so I've just tried the following and was able to confirm that using multipart-post
with Faraday WITHOUT faraday-multipart
is basically useless ๐
require 'faraday'
require 'multipart/post'
file = UploadIO.new(File.new('./.gitignore'), 'text', '.gitignore')
conn = Faraday.new('https://httpbingo.org')
conn.post('/anything/echo', { file: file })
which returns the following response body
{
"args": {},
"headers": { ... },
"method": "POST",
"url": "https://httpbingo.org/anything/echo",
"data": "file=%23%3CMultipart%3A%3APost%3A%3AUploadIO%3A0x000000011c694ed0%3E",
"files": {},
"form": {
"file": [
"#<Multipart::Post::UploadIO:0x000000011c694ed0>"
]
},
"json": null
}
Based on this, I don't think we need the gradual changes or backwards compatibility that was suggested in this comment, and instead we can just change the code to check for Faraday::UploadIO
instead of UploadIO
.
@jimeh @olleolleolle am I missing anything?
@iMacTia Yep, that all makes sense to me :)
No point in trying to be compatible with just the multipart-post
gem if it's utterly broken without faraday-multipart
... lol
Nice, nice!