Skip to content
Open
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ state (e.g., `assembly.completed?` vs. checking the result of
`assembly[:ok]`). Methods suffixed by a bang make a live query against the
Transloadit HTTP API.

Responses parse their JSON body once and discard that cached value when reloaded
or replaced. If the API or an upstream proxy returns invalid JSON, accessing
`body` or a response attribute raises `Transloadit::Exception::InvalidResponse`.
The exception exposes `status`, `raw_body`, and the original parser error through
`cause` when available. Its message includes the HTTP status without including the
response body. Treat the raw diagnostics as potentially sensitive when handling
the exception.

### 2. Uploading multiple files

Multiple files can be given to the `create!` method in order to upload more
Expand Down
15 changes: 15 additions & 0 deletions lib/transloadit/exception.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,19 @@ def message
"reload_until_finished! reached limit specified in :tries option. This is not a rate limit and you may continue to poll for updates."
end
end

# Exception raised when an API response does not contain valid JSON.
class InvalidResponse < StandardError
# @return [Integer] the HTTP status of the invalid response
attr_reader :status

# @return [String] the original body, which may contain sensitive diagnostics
attr_reader :raw_body

def initialize(status:, raw_body:)
@status = status
@raw_body = raw_body
super("Transloadit returned invalid JSON (HTTP #{status})")
end
end
end
12 changes: 11 additions & 1 deletion lib/transloadit/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,16 @@ def [](attribute)
# @return [Hash] the parsed JSON body hash
#
def body
MultiJson.load @raw_body
return @body if instance_variable_defined?(:@body)

# Some MultiJson adapters accept empty input as nil instead of reporting a parse error.
if @raw_body.to_s.strip.empty?
raise Transloadit::Exception::InvalidResponse.new(status: status, raw_body: @raw_body)
end

@body = MultiJson.load(@raw_body)
rescue MultiJson::ParseError => error
raise Transloadit::Exception::InvalidResponse.new(status: status, raw_body: @raw_body), cause: error
end

#
Expand Down Expand Up @@ -76,6 +85,7 @@ def replace(other)
@raw_body = other.raw_body
@headers = other.headers
@status = other.status
remove_instance_variable(:@body) if instance_variable_defined?(:@body)
self
end

Expand Down
74 changes: 74 additions & 0 deletions test/unit/transloadit/test_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
status: 200
)

_(response.body.fetch("ok")).must_equal "ASSEMBLY_EXECUTING"
returned = response.replace(replacement)

_(returned).must_be_same_as response
Expand All @@ -29,6 +30,79 @@
_(response.status).must_equal 200
end

it "must parse a successful body only once" do
response = Transloadit::Response.new(body: '{"ok":"ASSEMBLY_COMPLETED"}', headers: {}, status: 200)
calls = 0
parser = MultiJson.method(:load)
with_singleton_method(MultiJson, :load, proc { |raw|
calls += 1
parser.call(raw)
}) do
parsed = response.body
_(response.body).must_be_same_as parsed
_(response["ok"]).must_equal "ASSEMBLY_COMPLETED"
_(response.inspect).must_equal parsed.inspect
end

_(calls).must_equal 1
end

it "must preserve array bodies" do
response = Transloadit::Response.new(body: '[{"assembly_id":"assembly-id"}]', headers: {}, status: 200)

_(response.body).must_equal [{"assembly_id" => "assembly-id"}]
end

it "must memoize JSON null and false without changing their values" do
null_response = Transloadit::Response.new(body: "null", headers: {}, status: 200)
false_response = Transloadit::Response.new(body: "false", headers: {}, status: 200)
assert_nil null_response.body
_(false_response.body).must_equal false

with_singleton_method(MultiJson, :load, proc { |_raw| raise "Body was parsed again" }) do
assert_nil null_response.body
_(false_response.body).must_equal false
end
end

it "must expose invalid-response diagnostics without including the body in the message" do
raw_body = "<html>Proxy error: sensitive upstream diagnostics</html>"
response = Transloadit::Response.new(body: raw_body, headers: {}, status: 502)

error = assert_raises(Transloadit::Exception::InvalidResponse) { response.body }

_(error.status).must_equal 502
_(error.raw_body).must_equal raw_body
_(error.cause).must_be_kind_of MultiJson::ParseError
_(error.message).must_equal "Transloadit returned invalid JSON (HTTP 502)"
end

it "must wrap parse failures raised through attribute access" do
response = Transloadit::Response.new(body: "", headers: {}, status: 503)

error = assert_raises(Transloadit::Exception::InvalidResponse) { response["ok"] }
_(error.status).must_equal 503
end

it "must reject whitespace-only bodies" do
response = Transloadit::Response.new(body: " \n\t", headers: {}, status: 502)

assert_raises(Transloadit::Exception::InvalidResponse) { response.body }
end

it "must discard a parsed body when a replacement contains invalid JSON" do
response = Transloadit::Response.new(body: '{"ok":"ASSEMBLY_EXECUTING"}', headers: {}, status: 200)
_(response.body.fetch("ok")).must_equal "ASSEMBLY_EXECUTING"
replacement = Transloadit::Response.new(body: "upstream failure", headers: {}, status: 502)

response.replace(replacement)
error = assert_raises(Transloadit::Exception::InvalidResponse) { response.body }
_(error.status).must_equal 502

response.replace(Transloadit::Response.new(body: '{"ok":"ASSEMBLY_COMPLETED"}', headers: {}, status: 200))
_(response["ok"]).must_equal "ASSEMBLY_COMPLETED"
end

describe "when initialized" do
before do
VCR.use_cassette "fetch_assembly_ok" do
Expand Down
2 changes: 2 additions & 0 deletions transloadit.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Gem::Specification.new do |gem|

gem.add_dependency "faraday", ">= 2.0", "< 3.0"
gem.add_dependency "faraday-multipart", "~> 1.0"
# Ruby 3.1 uses MultiJson 1.19, whose positional parse options are incompatible with JSON 3.
gem.add_dependency "json", "< 3.0"
gem.add_dependency "multi_json"
gem.add_dependency "mime-types"

Expand Down
Loading