Skip to content

Commit 311fa8f

Browse files
author
Arnaud Lachaume
committed
base64 encode Cloud Task payload
1 parent 0d2e7d8 commit 311fa8f

File tree

7 files changed

+100
-17
lines changed

7 files changed

+100
-17
lines changed

app/controllers/cloudtasker/worker_controller.rb

+21-3
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ class WorkerController < ApplicationController
1616
# Run a worker from a Cloud Task payload
1717
#
1818
def run
19-
# Build payload
20-
payload = JSON.parse(request.body.read).merge(job_retries: job_retries)
21-
2219
# Process payload
2320
WorkerHandler.execute_from_payload!(payload)
2421
head :no_content
@@ -37,6 +34,27 @@ def run
3734

3835
private
3936

37+
#
38+
# Parse the request body and return the actual job
39+
# payload.
40+
#
41+
# @return [Hash] The job payload
42+
#
43+
def payload
44+
@payload ||= begin
45+
# Get raw body
46+
content = request.body.read
47+
48+
# Decode content if the body is Base64 encoded
49+
if request.headers[Cloudtasker::Config::ENCODING_HEADER].to_s.downcase == 'base64'
50+
content = Base64.decode64(content)
51+
end
52+
53+
# Return content parsed as JSON and add job retries count
54+
JSON.parse(content).merge(job_retries: job_retries)
55+
end
56+
end
57+
4058
#
4159
# Extract the number of times this task failed at runtime.
4260
#

examples/sinatra/app.rb

+6-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919
# Authenticate request
2020
Cloudtasker::Authenticator.verify!(request.env['HTTP_AUTHORIZATION'].to_s.split(' ').last)
2121

22-
# Build payload
23-
payload = JSON.parse(request.body.read, symbolize_names: true)
24-
.slice(:worker, :job_id, :job_args, :job_meta, :job_queue)
22+
# Capture content and decode content
23+
content = request.body.read
24+
content = Base64.decode64(content) if request.env['HTTP_CONTENT_TRANSFER_ENCODING'].to_s.downcase == 'base64'
25+
26+
# Format job payload
27+
payload = JSON.parse(content)
2528
.merge(job_retries: request.env['HTTP_X_CLOUDTASKS_TASKEXECUTIONCOUNT'].to_i)
2629

2730
# Process payload

lib/cloudtasker/backend/google_cloud_task.rb

+24-4
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,29 @@ def self.format_schedule_time(schedule_time)
8282
Google::Protobuf::Timestamp.new.tap { |e| e.seconds = schedule_time.to_i }
8383
end
8484

85+
#
86+
# Format the job payload sent to Cloud Tasks.
87+
#
88+
# @param [Hash] hash The worker payload.
89+
#
90+
# @return [Hash] The Cloud Task payloadd.
91+
#
92+
def self.format_task_payload(payload)
93+
payload = JSON.parse(payload.to_json, symbolize_names: true) # deep dup
94+
95+
# Format schedule time to Google Protobuf timestamp
96+
payload[:schedule_time] = format_schedule_time(payload[:schedule_time])
97+
98+
# Encode job content to support UTF-8. Google Cloud Task
99+
# expect content to be ASCII-8BIT compatible (binary)
100+
payload[:http_request][:headers] ||= {}
101+
payload[:http_request][:headers][Cloudtasker::Config::CONTENT_TYPE_HEADER] = 'text/json'
102+
payload[:http_request][:headers][Cloudtasker::Config::ENCODING_HEADER] = 'Base64'
103+
payload[:http_request][:body] = Base64.encode64(payload[:http_request][:body])
104+
105+
payload
106+
end
107+
85108
#
86109
# Find a task by id.
87110
#
@@ -104,10 +127,7 @@ def self.find(id)
104127
# @return [Cloudtasker::Backend::GoogleCloudTask, nil] The created task.
105128
#
106129
def self.create(payload)
107-
# Format payload
108-
payload = payload.merge(
109-
schedule_time: format_schedule_time(payload[:schedule_time])
110-
).compact
130+
payload = format_task_payload(payload)
111131

112132
# Extract relative queue name
113133
relative_queue = payload.delete(:queue)

lib/cloudtasker/config.rb

+9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ class Config
1212
# Retry header in Cloud Task responses
1313
RETRY_HEADER = 'X-CloudTasks-TaskExecutionCount'
1414

15+
# Content-Transfer-Encoding header in Cloud Task responses
16+
ENCODING_HEADER = 'Content-Transfer-Encoding'
17+
18+
# Content Type
19+
CONTENT_TYPE_HEADER = 'Content-Type'
20+
21+
# Authorization header
22+
AUTHORIZATION_HEADER = 'Authorization'
23+
1524
# Default values
1625
DEFAULT_LOCATION_ID = 'us-east1'
1726
DEFAULT_PROCESSOR_PATH = '/cloudtasker/run'

lib/cloudtasker/worker_handler.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ def task_payload
4242
http_method: 'POST',
4343
url: Cloudtasker.config.processor_url,
4444
headers: {
45-
'Content-Type' => 'application/json',
46-
'Authorization' => "Bearer #{Authenticator.verification_token}"
45+
Cloudtasker::Config::CONTENT_TYPE_HEADER => 'application/json',
46+
Cloudtasker::Config::AUTHORIZATION_HEADER => "Bearer #{Authenticator.verification_token}"
4747
},
4848
body: worker_payload.to_json
4949
},

spec/cloudtasker/backend/google_cloud_task_spec.rb

+22-4
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,21 @@
141141
end
142142
end
143143

144+
describe '.format_task_payload' do
145+
subject { described_class.format_task_payload(job_payload) }
146+
147+
let(:expected_payload) do
148+
payload = JSON.parse(job_payload.to_json, symbolize_names: true)
149+
payload[:schedule_time] = described_class.format_schedule_time(job_payload[:schedule_time])
150+
payload[:http_request][:headers]['Content-Type'] = 'text/json'
151+
payload[:http_request][:headers]['Content-Transfer-Encoding'] = 'Base64'
152+
payload[:http_request][:body] = Base64.encode64(job_payload[:http_request][:body])
153+
payload
154+
end
155+
156+
it { is_expected.to eq(expected_payload) }
157+
end
158+
144159
describe '.find' do
145160
subject { described_class.find(id) }
146161

@@ -170,10 +185,13 @@
170185
let(:resp) { instance_double('Google::Cloud::Tasks::V2beta3::Task') }
171186
let(:task) { instance_double(described_class.to_s) }
172187
let(:expected_payload) do
173-
job_payload.merge(
174-
schedule_time: described_class.format_schedule_time(job_payload[:schedule_time]),
175-
queue: nil
176-
).compact
188+
payload = JSON.parse(job_payload.to_json, symbolize_names: true)
189+
payload.delete(:queue)
190+
payload[:schedule_time] = described_class.format_schedule_time(job_payload[:schedule_time])
191+
payload[:http_request][:headers]['Content-Type'] = 'text/json'
192+
payload[:http_request][:headers]['Content-Transfer-Encoding'] = 'Base64'
193+
payload[:http_request][:body] = Base64.encode64(job_payload[:http_request][:body])
194+
payload
177195
end
178196

179197
before { allow(described_class).to receive(:queue_path).with(job_payload[:queue]).and_return(queue) }

spec/cloudtasker/worker_controller_spec.rb

+16-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
routes { Cloudtasker::Engine.routes }
55

66
describe 'POST #run' do
7-
subject { post :run, body: payload.to_json, as: :json }
7+
subject { post :run, body: request_body, as: :json }
88

99
let(:payload) do
1010
{
@@ -16,6 +16,7 @@
1616
'other' => 'foo'
1717
}
1818
end
19+
let(:request_body) { payload.to_json }
1920
let(:expected_payload) { payload.merge(job_retries: retries) }
2021
let(:id) { '111' }
2122
let(:worker_class_name) { 'TestWorker' }
@@ -38,6 +39,20 @@
3839
it { is_expected.to be_successful }
3940
end
4041

42+
context 'with base64 encoded body' do
43+
let(:request_body) { Base64.encode64(payload.to_json) }
44+
45+
before do
46+
request.env['HTTP_AUTHORIZATION'] = "Bearer #{auth_token}"
47+
request.env['HTTP_CONTENT_TRANSFER_ENCODING'] = 'BASE64'
48+
allow(Cloudtasker::WorkerHandler).to receive(:execute_from_payload!)
49+
.with(expected_payload)
50+
.and_return(true)
51+
end
52+
after { expect(Cloudtasker::WorkerHandler).to have_received(:execute_from_payload!) }
53+
it { is_expected.to be_successful }
54+
end
55+
4156
context 'with execution errors' do
4257
before do
4358
request.env['HTTP_AUTHORIZATION'] = "Bearer #{auth_token}"

0 commit comments

Comments
 (0)