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
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,20 @@ def initialize(api_url, auth_secret)
end

# rubocop:disable Metrics/ParameterLists
def call_rpc(endpoint, caller: nil, method: :get, payload: nil, symbolize_keys: false, if_none_match: nil)
def call_rpc(endpoint, caller: nil, method: :get, payload: nil, symbolize_keys: false,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many parameters (count = 7): call_rpc [qlty:function-parameters]

if_none_match: nil, with_response: false)
response = make_request(endpoint, caller: caller, method: method, payload: payload,
symbolize_keys: symbolize_keys, if_none_match: if_none_match)
handle_response(response)
end
return NotModified if response.status == HTTP_NOT_MODIFIED

raise_appropriate_error(response) unless response.success?

with_response ? response : response.body
end
# rubocop:enable Metrics/ParameterLists

def fetch_schema(endpoint, if_none_match: nil)
response = make_request(endpoint, method: :get, symbolize_keys: true, if_none_match: if_none_match)
handle_response(response)
call_rpc(endpoint, method: :get, symbolize_keys: true, if_none_match: if_none_match)
end

private
Expand Down Expand Up @@ -111,13 +114,6 @@ def generate_signature(timestamp)
OpenSSL::HMAC.hexdigest('SHA256', @auth_secret, timestamp)
end

def handle_response(response)
return response.body if response.success?
return NotModified if response.status == HTTP_NOT_MODIFIED

raise_appropriate_error(response)
end

def raise_appropriate_error(response)
error_body = parse_error_body(response)
status = response.status
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
require 'base64'
require 'cgi'
require 'json'
require 'stringio'

module ForestAdminDatasourceRpc
class Collection < ForestAdminDatasourceToolkit::Collection
Expand Down Expand Up @@ -127,7 +130,12 @@ def execute(caller, name, data, filter = nil)
"Forwarding '#{@name}' action #{name} call to the Rpc agent on #{url}."
)

@client.call_rpc(url, caller: caller, method: :post, payload: params, symbolize_keys: true)
response = @client.call_rpc(url, caller: caller, method: :post, payload: params,
symbolize_keys: true, with_response: true)

return build_file_result(response) if response.headers['x-forest-action-type'] == 'File'

response.body
end

def get_form(caller, name, data = nil, filter = nil, metas = nil)
Expand Down Expand Up @@ -176,5 +184,20 @@ def encode_form_data(data)
end
end
end

def build_file_result(response)
response_headers_header = response.headers['x-forest-action-response-headers']
file_name_header = response.headers['x-forest-action-file-name']

result = {
type: 'File',
mime_type: response.headers['content-type'],
name: CGI.unescape(file_name_header.to_s),
stream: response.body.to_s
}
result[:response_headers] = JSON.parse(response_headers_header) if response_headers_header
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low forest_admin_datasource_rpc/collection.rb:198

When response_headers_header is an empty string or invalid JSON, JSON.parse raises JSON::ParserError and crashes action execution. The condition if response_headers_header is truthy for empty strings, so an absent or malformed header causes a hard failure rather than graceful fallback.

-      result[:response_headers] = JSON.parse(response_headers_header) if response_headers_header
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/collection.rb around line 198:

When `response_headers_header` is an empty string or invalid JSON, `JSON.parse` raises `JSON::ParserError` and crashes action execution. The condition `if response_headers_header` is truthy for empty strings, so an absent or malformed header causes a hard failure rather than graceful fallback.

Evidence trail:
packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/collection.rb lines 188-201 at REVIEWED_COMMIT: `response_headers_header` set from `response.headers['x-forest-action-response-headers']` (line 189), guard on line 198 `if response_headers_header` is truthy for empty strings in Ruby, `JSON.parse("")` raises `JSON::ParserError`. Ruby semantics: only `nil` and `false` are falsy; empty string is truthy.


result
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ module ForestAdminDatasourceRpc
allow(Utils::RpcClient).to receive(:new).and_return(rpc_client)
end

let(:raw_response) do
instance_double(Faraday::Response, body: {}, headers: {}, status: 200, success?: true)
end
let(:rpc_client) { instance_double(Utils::RpcClient, call_rpc: {}) }

before do
allow(rpc_client).to receive(:call_rpc).with(any_args, hash_including(with_response: true)).and_return(raw_response)
end
let(:datasource) { Datasource.new({ uri: 'http://localhost' }, introspection) }
let(:collection) { datasource.get_collection('Product') }
let(:caller) { build_caller }
Expand Down Expand Up @@ -239,24 +246,77 @@ module ForestAdminDatasourceRpc

expect(rpc_client).to have_received(:call_rpc) do |_url, options|
expect(options[:symbolize_keys]).to be(true)
expect(options[:with_response]).to be(true)
end
end

it 'returns the action result as-is so :type and other keys reach ActionResult.parse' do
it 'returns the parsed body so :type and other keys reach ActionResult.parse' do
success_result = {
type: 'Success',
message: 'ok',
invalidated: ['books'],
html: nil,
response_headers: {}
}
allow(rpc_client).to receive(:call_rpc).and_return(success_result)
allow(raw_response).to receive(:body).and_return(success_result)

result = collection.execute(caller, 'my_action', {})

expect(result).to eq(success_result)
expect(result[:type]).to eq('Success')
end

context 'when the server replies with X-Forest-Action-Type=File' do
let(:file_body) { 'binary-payload' }
let(:raw_response) do
instance_double(
Faraday::Response,
body: file_body,
headers: {
'content-type' => 'application/pdf',
'x-forest-action-type' => 'File',
'x-forest-action-file-name' => CGI.escape('report final.pdf'),
'x-forest-action-response-headers' => { 'set-cookie' => 'token=xyz' }.to_json
},
status: 200,
success?: true
)
end

it 'rebuilds a File action result from the response headers and body' do
result = collection.execute(caller, 'download', {})

expect(result[:type]).to eq('File')
expect(result[:mime_type]).to eq('application/pdf')
expect(result[:name]).to eq('report final.pdf')
expect(result[:response_headers]).to eq({ 'set-cookie' => 'token=xyz' })
expect(result[:stream]).to eq(file_body)
end

context 'when response_headers header is absent' do
let(:raw_response) do
instance_double(
Faraday::Response,
body: 'hi',
headers: {
'content-type' => 'text/plain',
'x-forest-action-type' => 'File',
'x-forest-action-file-name' => 'note.txt'
},
status: 200,
success?: true
)
end

it 'omits response_headers from the rebuilt result' do
result = collection.execute(caller, 'download', {})

expect(result[:type]).to eq('File')
expect(result[:name]).to eq('note.txt')
expect(result).not_to have_key(:response_headers)
end
end
end
end

context 'when call get_form' do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,24 @@ module Utils

expect(result).to eq({})
end

context 'when called with with_response: true' do
it 'returns the Faraday::Response object instead of the body' do
result = rpc_client.call_rpc('/rpc/test', method: :post, with_response: true)

expect(result).to be(response)
expect(result.body).to eq({})
expect(result.headers).to eq(response_headers)
end
end

context 'when called without with_response (default)' do
it 'returns the response body for backward compatibility' do
result = rpc_client.call_rpc('/rpc/test', method: :post)

expect(result).to eq({})
end
end
end

describe '#fetch_schema' do
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'cgi'
require 'json'
require 'jsonapi-serializers'

module ForestAdminRpcAgent
Expand All @@ -20,7 +22,30 @@ def handle_request(args)
data = args[:params]['data']
action = args[:params]['action']

collection.execute(args[:caller], action, data, filter)
result = collection.execute(args[:caller], action, data, filter)

return build_file_response(result) if file_result?(result)

result
end

private

def file_result?(result)
result.is_a?(Hash) && result[:type] == 'File'
end

def build_file_response(result)
encoded_name = CGI.escape(result[:name].to_s)
headers = {
'Content-Type' => result[:mime_type],
'Content-Disposition' => %(attachment; filename="#{encoded_name}"),
'X-Forest-Action-Type' => 'File',
'X-Forest-Action-File-Name' => encoded_name
}
headers['X-Forest-Action-Response-Headers'] = result[:response_headers].to_json if result[:response_headers]

{ status: 200, headers: headers, content: result[:stream] }
end
end
end
Expand Down
Loading