Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
### Unreleased

- BREAKING: Replace the long-dormant `rest-client` transport with Faraday 2 and `faraday-multipart`.
- Keep HTTP client types out of SDK responses and exceptions while preserving response status, headers, body access, multipart ordering, and rate-limit retries.
- Add a [v3 to v4 migration guide](MIGRATING_TO_V4.md).

### 3.1.2 / 2025-10-29

- Compress e2e fixture asset to keep gem size close to previous releases (kvz)
Expand Down
135 changes: 135 additions & 0 deletions MIGRATING_TO_V4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Migrating from v3 to v4

Version 4 replaces the `rest-client` HTTP transport with Faraday 2. Most applications only use the
Transloadit API exposed by this gem and do not need code changes. The migration is a major release
because v3 also delegated undocumented methods to RestClient response objects and exposed
RestClient exception inheritance.

The minimum supported Ruby version remains 3.1.

## Update the dependency

Once v4 is released, update the application Gemfile and bundle:

```ruby
gem "transloadit", "~> 4.0"
```

```shell
bundle update transloadit
```

The SDK installs Faraday and its multipart middleware. Applications do not need to add Faraday
directly. Remove an explicit `rest-client` dependency only if the application does not use it for
anything else. Version 4 no longer installs or loads the `RestClient` constant on the application's
behalf.

## Responses

Use the SDK-owned response API:

```ruby
response = transloadit.assembly.get(assembly_id)

response["ok"]
response[:assembly_id]
response.body
response.headers
response.code
response.status
```

`code` continues to return the integer HTTP status, and `status` is its new explicit alias. Header
names remain normalized to lowercase symbols with underscores, for example
`response.headers[:retry_after]`.

HTTP error statuses still return `Transloadit::Response` objects. They are not converted into
transport exceptions:

```ruby
response = transloadit.assembly.get(assembly_id)

if response.code >= 400
warn response["error"]
end
```

### Remove raw RestClient response usage

In v3, `Transloadit::Response` delegated unknown methods to a `RestClient::Response`. Version 4 no
longer exposes the underlying HTTP client. Replace calls to RestClient-specific response methods
with the SDK-owned response API above.

For example:

```ruby
# v3: relied on the delegated RestClient object
response.raw_headers

# v4
response.headers
```

If an application relies on another delegated method that has no equivalent, open an issue with the
use case before upgrading.

## Request failures

Network, TLS, and timeout failures now raise `Transloadit::Exception::RequestFailed` instead of a
RestClient exception:

```ruby
# v3
begin
transloadit.assembly.get(assembly_id)
rescue RestClient::Exception => error
warn error.message
end

# v4
begin
transloadit.assembly.get(assembly_id)
rescue Transloadit::Exception::RequestFailed => error
warn error.message
end
```

The original adapter exception is retained as `error.cause` for diagnostics. Avoid branching on its
class so application behavior does not become coupled to Faraday.

## Rate-limit failures

Continue rescuing the SDK exception directly:

```ruby
begin
assembly.create!(file)
rescue Transloadit::Exception::RateLimitReached => error
retry_after = error.response.wait_time
end
```

`RateLimitReached` remains a `StandardError` and retains its Transloadit response through
`error.response`, but it no longer inherits from `RestClient::RequestEntityTooLarge`. Replace code
that rescues the RestClient superclass with the SDK exception.

## Uploaded file handles

Version 4 does not close path-backed file objects supplied by the caller. Prefer a block so ownership
is explicit:

```ruby
File.open("video.mp4", "rb") do |file|
assembly.create!(file)
end
```

Multipart field order is unchanged: `params` and `signature` are sent before file bodies.

## Upgrade checklist

- Update the gem and run the application's request and upload tests.
- Replace rescues of `RestClient::Exception` and `RestClient::RequestEntityTooLarge`.
- Replace calls to delegated RestClient response methods.
- Confirm the application closes files that it opens.
- Remove the direct `rest-client` dependency if nothing else uses it.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ _If you run Ruby on Rails and are looking to integrate with the browser for file
gem install transloadit
```

Upgrading from v3? See the [v4 migration guide](MIGRATING_TO_V4.md) for response, exception, and
file-handle compatibility notes.

## Usage

To get started, you need to require the 'transloadit' gem:
Expand Down
2 changes: 0 additions & 2 deletions lib/transloadit/assembly.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,6 @@ def _handle_rate_limit!(response, ios, is_retrying)
if is_retrying
warn "Rate limit reached. Waiting for #{response.wait_time} seconds before retrying."
sleep response.wait_time
# RestClient closes file streams at the end of a request.
ios.collect! { |file| File.open file.path }
else
raise Transloadit::Exception::RateLimitReached.new(response)
end
Expand Down
20 changes: 14 additions & 6 deletions lib/transloadit/exception.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
require "transloadit"

require "rest-client"

module Transloadit::Exception
#
# Exception raised when an HTTP request cannot be completed.
# The original transport error is available through +cause+.
#
class RequestFailed < StandardError
end

#
# Exception raised when Rate limit error response is returned from the API.
# See {Rate Limiting}[https://transloadit.com/docs/api-docs/#rate-limiting]
#
class RateLimitReached < RestClient::RequestEntityTooLarge
def default_message
retry_msg = " Retry in #{@response.wait_time} seconds" if @response
"Transloadit Rate Limit Reached.#{retry_msg}"
class RateLimitReached < StandardError
# @return [Transloadit::Response] the API response that reported the rate limit
attr_reader :response

def initialize(response)
@response = response
super("Transloadit Rate Limit Reached. Retry in #{response.wait_time} seconds")
end
end

Expand Down
76 changes: 54 additions & 22 deletions lib/transloadit/request.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require "transloadit"

require "rest-client"
require "faraday"
require "faraday/multipart"
require "mime/types"
require "openssl"

#
Expand Down Expand Up @@ -47,7 +49,7 @@ def initialize(url, secret = nil)
#
def get(params = {})
request! do
api[url.path + to_query(params)].get(API_HEADERS)
api.get(url.path + to_query(params), nil, API_HEADERS)
end
end

Expand All @@ -60,8 +62,10 @@ def get(params = {})
#
def delete(payload = {})
request! do
options = {payload: to_payload(payload)}
api(options)[url.path].delete(API_HEADERS)
api.delete(url.path) do |request|
request.headers.update(API_HEADERS)
request.body = to_payload(payload)
end
end
end

Expand All @@ -74,7 +78,7 @@ def delete(payload = {})
#
def post(payload = {})
request! do
api[url.path].post(to_payload(payload), API_HEADERS)
api.post(url.path, to_payload(payload), API_HEADERS)
end
end

Expand All @@ -87,7 +91,7 @@ def post(payload = {})
#
def put(payload = {})
request! do
api[url.path].put(to_payload(payload), API_HEADERS)
api.put(url.path, to_payload(payload), API_HEADERS)
end
end

Expand Down Expand Up @@ -118,12 +122,20 @@ def self._hmac(key, message)
# hostname, then the hostname is used as the base endpoint of the API.
# Otherwise uses the class-level API base.
#
# @return [RestClient::Resource] the API endpoint for this instance
# @return [Faraday::Connection] the API endpoint for this instance
#
def api(options = {})
def api
@api ||= case url.host
when String then RestClient::Resource.new("#{url.scheme}://#{url.host}", options)
else RestClient::Resource.new("#{API_ENDPOINT.scheme}://#{API_ENDPOINT.host}", options)
when String then connection_for("#{url.scheme}://#{url.host}")
else connection_for("#{API_ENDPOINT.scheme}://#{API_ENDPOINT.host}")
end
end

def connection_for(base_url)
Faraday.new(url: base_url) do |connection|
connection.request :multipart
connection.request :url_encoded
connection.adapter Faraday.default_adapter
end
end

Expand Down Expand Up @@ -151,12 +163,33 @@ def to_payload(payload = nil)
sig = signature(new_payload[:params])
new_payload[:signature] = sig unless sig.nil?

# Copy all values, excluding :params and :signature keys.
new_payload.update payload.reject { |key, _| key == :params || key == :signature }
payload.each do |key, value|
next if key == :params || key == :signature

new_payload[key] = upload?(value) ? multipart_file(value) : value
end

new_payload
end

def upload?(value)
value.respond_to?(:read)
end

def multipart_file(file)
path = file.path if file.respond_to?(:path)
filename = file.original_filename if file.respond_to?(:original_filename)
filename = File.basename(path) if filename.to_s.empty? && path
filename = "upload" if filename.to_s.empty?

content_type = file.content_type if file.respond_to?(:content_type)
content_type = MIME::Types.type_for(path).first&.content_type if content_type.to_s.empty? && path
content_type = "application/octet-stream" if content_type.to_s.empty?

source = path || file
Faraday::Multipart::FilePart.new(source, content_type, filename)
end

#
# Updates the GET/DELETE params hash to be compliant with the Transloadit
# API by URI escaping and encoding the params hash, and attaching a
Expand All @@ -181,18 +214,17 @@ def to_query(params = nil)
end

#
# Wraps a request's results in a Transloadit::Response, even if an exception
# is raised by RestClient.
# Wraps a request's result in a Transloadit::Response.
#
def request!(&request)
Transloadit::Response.new yield
rescue RestClient::Exception => e
# The response attribute can be nil, for example for RestClient::Exceptions::OpenTimeout exceptions.
# Then, we cannot convert them into a Transloadit::Response, so instead we raise them again for
# the user to be visible.
# See https://github.com/transloadit/ruby-sdk/issues/53
raise e if e.response.nil?
Transloadit::Response.new e.response
response = yield
Transloadit::Response.new(
body: response.body,
headers: response.headers,
status: response.status
)
rescue Faraday::Error
raise Transloadit::Exception::RequestFailed, "Transloadit request failed"
end

#
Expand Down
Loading
Loading