Skip to content

Commit be29d80

Browse files
committed
Fix HTTParty content type for nil request bodies (#536)
- Default POST, PUT, PATCH, DELETE payload to {} when request_body is nil to ensure Content-Type: application/json is sent - Add request_body support to DELETE (e.g. cancellation_reason for bookings)
1 parent 77de434 commit be29d80

7 files changed

Lines changed: 191 additions & 50 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
### Unreleased
4+
* Fixed HTTParty content type issue when request body is nil - POST, PUT, PATCH, and DELETE now default to empty object to ensure Content-Type: application/json is sent (#536)
5+
* Added support for request_body parameter on DELETE (e.g. cancellation_reason for bookings) (#536)
6+
37
### [6.7.0]
48
* Added access to response headers
59

lib/nylas/handler/api_operations.rb

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,20 @@ module Post
5858
protected
5959

6060
include HttpClient
61-
# Performs a POST call to the Nylas API.
62-
#
63-
# @param path [String] Destination path for the call.
64-
# @param query_params [Hash, {}] Query params to pass to the call.
65-
# @param request_body [Hash, nil] Request body to pass to the call.
66-
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
61+
# Performs a POST call to the Nylas API.
62+
#
63+
# @param path [String] Destination path for the call.
64+
# @param query_params [Hash, {}] Query params to pass to the call.
65+
# @param request_body [Hash, nil] Request body to pass to the call.
66+
# Defaults to {} when nil to ensure Content-Type: application/json is sent.
67+
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
6768
# @return [Array(Hash, String, Hash)] Nylas data object, API Request ID, and response headers.
6869
def post(path:, query_params: {}, request_body: nil, headers: {})
6970
response = execute(
7071
method: :post,
7172
path: path,
7273
query: query_params,
73-
payload: request_body,
74+
payload: request_body || {},
7475
headers: headers,
7576
api_key: api_key,
7677
timeout: timeout
@@ -85,19 +86,20 @@ module Put
8586
protected
8687

8788
include HttpClient
88-
# Performs a PUT call to the Nylas API.
89-
#
90-
# @param path [String] Destination path for the call.
91-
# @param query_params [Hash, {}] Query params to pass to the call.
92-
# @param request_body [Hash, nil] Request body to pass to the call.
93-
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
89+
# Performs a PUT call to the Nylas API.
90+
#
91+
# @param path [String] Destination path for the call.
92+
# @param query_params [Hash, {}] Query params to pass to the call.
93+
# @param request_body [Hash, nil] Request body to pass to the call.
94+
# Defaults to {} when nil to ensure Content-Type: application/json is sent.
95+
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
9496
# @return Nylas data object and API Request ID.
9597
def put(path:, query_params: {}, request_body: nil, headers: {})
9698
response = execute(
9799
method: :put,
98100
path: path,
99101
query: query_params,
100-
payload: request_body,
102+
payload: request_body || {},
101103
headers: headers,
102104
api_key: api_key,
103105
timeout: timeout
@@ -112,19 +114,20 @@ module Patch
112114
protected
113115

114116
include HttpClient
115-
# Performs a PATCH call to the Nylas API.
116-
#
117-
# @param path [String] Destination path for the call.
118-
# @param query_params [Hash, {}] Query params to pass to the call.
119-
# @param request_body [Hash, nil] Request body to pass to the call.
120-
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
117+
# Performs a PATCH call to the Nylas API.
118+
#
119+
# @param path [String] Destination path for the call.
120+
# @param query_params [Hash, {}] Query params to pass to the call.
121+
# @param request_body [Hash, nil] Request body to pass to the call.
122+
# Defaults to {} when nil to ensure Content-Type: application/json is sent.
123+
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
121124
# @return Nylas data object and API Request ID.
122125
def patch(path:, query_params: {}, request_body: nil, headers: {})
123126
response = execute(
124127
method: :patch,
125128
path: path,
126129
query: query_params,
127-
payload: request_body,
130+
payload: request_body || {},
128131
headers: headers,
129132
api_key: api_key,
130133
timeout: timeout
@@ -139,25 +142,27 @@ module Delete
139142
protected
140143

141144
include HttpClient
142-
# Performs a DELETE call to the Nylas API.
143-
#
144-
# @param path [String] Destination path for the call.
145-
# @param query_params [Hash, {}] Query params to pass to the call.
146-
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
147-
# @return Nylas data object and API Request ID.
148-
def delete(path:, query_params: {}, headers: {})
149-
response = execute(
150-
method: :delete,
151-
path: path,
152-
query: query_params,
153-
headers: headers,
154-
payload: nil,
155-
api_key: api_key,
156-
timeout: timeout
157-
)
158-
159-
[response[:data], response[:request_id]]
160-
end
145+
# Performs a DELETE call to the Nylas API.
146+
#
147+
# @param path [String] Destination path for the call.
148+
# @param query_params [Hash, {}] Query params to pass to the call.
149+
# @param request_body [Hash, nil] Optional request body (e.g. cancellation_reason for bookings).
150+
# Defaults to {} to ensure Content-Type: application/json is sent.
151+
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
152+
# @return Nylas data object and API Request ID.
153+
def delete(path:, query_params: {}, request_body: nil, headers: {})
154+
response = execute(
155+
method: :delete,
156+
path: path,
157+
query: query_params,
158+
headers: headers,
159+
payload: request_body || {},
160+
api_key: api_key,
161+
timeout: timeout
162+
)
163+
164+
[response[:data], response[:request_id]]
165+
end
161166
end
162167
end
163168
end

lib/nylas/resources/bookings.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,13 @@ def confirm(booking_id:, request_body:, query_params: nil)
6464
# Delete a booking.
6565
# @param booking_id [String] The id of the booking to delete.
6666
# @param query_params [Hash, nil] Query params to pass to the request.
67+
# @param request_body [Hash, nil] Optional body params (e.g. cancellation_reason).
6768
# @return [Array(TrueClass, String)] True and the API Request ID for the delete operation.
68-
def destroy(booking_id:, query_params: nil)
69+
def destroy(booking_id:, query_params: nil, request_body: nil)
6970
_, request_id = delete(
7071
path: "#{api_uri}/v3/scheduling/bookings/#{booking_id}",
71-
query_params: query_params
72+
query_params: query_params,
73+
request_body: request_body
7274
)
7375

7476
[true, request_id]

spec/nylas/handler/api_operations_spec.rb

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def initialize(api_key, api_uri, timeout)
143143
method: :post,
144144
path: path,
145145
query: {},
146-
payload: nil,
146+
payload: {},
147147
headers: {},
148148
api_key: api_key,
149149
timeout: timeout
@@ -164,7 +164,7 @@ def initialize(api_key, api_uri, timeout)
164164
method: :post,
165165
path: path,
166166
query: {},
167-
payload: nil,
167+
payload: {},
168168
headers: {},
169169
api_key: api_key,
170170
timeout: timeout
@@ -174,6 +174,22 @@ def initialize(api_key, api_uri, timeout)
174174

175175
expect(response).to eq([mock_response[:data], mock_response[:request_id], nil])
176176
end
177+
178+
it "passes request_body to execute when provided" do
179+
path = "#{api_uri}/path"
180+
request_body = { foo: "bar" }
181+
allow(api_operations).to receive(:execute).with(
182+
method: :post,
183+
path: path,
184+
query: {},
185+
payload: request_body,
186+
headers: {},
187+
api_key: api_key,
188+
timeout: timeout
189+
).and_return(mock_response)
190+
191+
api_operations.send(:post, path: path, request_body: request_body)
192+
end
177193
end
178194
end
179195

@@ -206,7 +222,7 @@ def initialize(api_key, api_uri, timeout)
206222
method: :put,
207223
path: path,
208224
query: {},
209-
payload: nil,
225+
payload: {},
210226
headers: {},
211227
api_key: api_key,
212228
timeout: timeout
@@ -216,6 +232,22 @@ def initialize(api_key, api_uri, timeout)
216232

217233
expect(response).to eq([mock_response[:data], mock_response[:request_id]])
218234
end
235+
236+
it "passes request_body to execute when provided" do
237+
path = "#{api_uri}/path"
238+
request_body = { foo: "bar" }
239+
allow(api_operations).to receive(:execute).with(
240+
method: :put,
241+
path: path,
242+
query: {},
243+
payload: request_body,
244+
headers: {},
245+
api_key: api_key,
246+
timeout: timeout
247+
).and_return(mock_response)
248+
249+
api_operations.send(:put, path: path, request_body: request_body)
250+
end
219251
end
220252
end
221253

@@ -248,7 +280,7 @@ def initialize(api_key, api_uri, timeout)
248280
method: :patch,
249281
path: path,
250282
query: {},
251-
payload: nil,
283+
payload: {},
252284
headers: {},
253285
api_key: api_key,
254286
timeout: timeout
@@ -258,6 +290,22 @@ def initialize(api_key, api_uri, timeout)
258290

259291
expect(response).to eq([mock_response[:data], mock_response[:request_id]])
260292
end
293+
294+
it "passes request_body to execute when provided" do
295+
path = "#{api_uri}/path"
296+
request_body = { foo: "bar" }
297+
allow(api_operations).to receive(:execute).with(
298+
method: :patch,
299+
path: path,
300+
query: {},
301+
payload: request_body,
302+
headers: {},
303+
api_key: api_key,
304+
timeout: timeout
305+
).and_return(mock_response)
306+
307+
api_operations.send(:patch, path: path, request_body: request_body)
308+
end
261309
end
262310
end
263311

@@ -271,7 +319,7 @@ def initialize(api_key, api_uri, timeout)
271319
method: :delete,
272320
path: path,
273321
query: query_params,
274-
payload: nil,
322+
payload: {},
275323
headers: headers,
276324
api_key: api_key,
277325
timeout: timeout
@@ -288,7 +336,7 @@ def initialize(api_key, api_uri, timeout)
288336
method: :delete,
289337
path: path,
290338
query: {},
291-
payload: nil,
339+
payload: {},
292340
headers: {},
293341
api_key: api_key,
294342
timeout: timeout
@@ -298,6 +346,22 @@ def initialize(api_key, api_uri, timeout)
298346

299347
expect(response).to eq([mock_response[:data], mock_response[:request_id]])
300348
end
349+
350+
it "passes request_body to execute when provided" do
351+
path = "#{api_uri}/path"
352+
request_body = { cancellation_reason: "Meeting cancelled" }
353+
allow(api_operations).to receive(:execute).with(
354+
method: :delete,
355+
path: path,
356+
query: {},
357+
payload: request_body,
358+
headers: {},
359+
api_key: api_key,
360+
timeout: timeout
361+
).and_return(mock_response)
362+
363+
api_operations.send(:delete, path: path, request_body: request_body)
364+
end
301365
end
302366
end
303367
end

spec/nylas/handler/http_client_integration_spec.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,46 @@ class TestHttpClientIntegration
7575
end
7676
end
7777

78+
describe "Integration Tests - Content-Type for body-less requests (HTTParty fix)" do
79+
it "sends Content-Type: application/json for POST with empty payload" do
80+
stub_request(:post, "https://test.api.nylas.com/v3/connect/revoke")
81+
.with(
82+
body: "{}",
83+
headers: { "Content-Type" => "application/json" }
84+
)
85+
.to_return(status: 200, body: "{}", headers: { "Content-Type" => "application/json" })
86+
87+
http_client.send(:execute,
88+
method: :post,
89+
path: "https://test.api.nylas.com/v3/connect/revoke",
90+
timeout: 30,
91+
payload: {},
92+
api_key: "fake-key")
93+
94+
expect(WebMock).to have_requested(:post, "https://test.api.nylas.com/v3/connect/revoke")
95+
.with(headers: { "Content-Type" => "application/json" }, body: "{}")
96+
end
97+
98+
it "sends Content-Type: application/json for DELETE with empty payload" do
99+
stub_request(:delete, "https://test.api.nylas.com/v3/scheduling/bookings/booking-123")
100+
.with(
101+
body: "{}",
102+
headers: { "Content-Type" => "application/json" }
103+
)
104+
.to_return(status: 200, body: "{}", headers: { "Content-Type" => "application/json" })
105+
106+
http_client.send(:execute,
107+
method: :delete,
108+
path: "https://test.api.nylas.com/v3/scheduling/bookings/booking-123",
109+
timeout: 30,
110+
payload: {},
111+
api_key: "fake-key")
112+
113+
expect(WebMock).to have_requested(:delete, "https://test.api.nylas.com/v3/scheduling/bookings/booking-123")
114+
.with(headers: { "Content-Type" => "application/json" }, body: "{}")
115+
end
116+
end
117+
78118
describe "Integration Tests - backwards compatibility" do
79119
it "maintains the same response format as rest-client" do
80120
response_json = { "data" => { "id" => "123", "name" => "test" } }

spec/nylas/handler/http_client_spec.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ class TestHttpClient
126126
end
127127

128128
context "when building request with a payload" do
129+
it "returns the correct request with empty json payload and sets Content-Type" do
130+
payload = {}
131+
request = http_client.send(:build_request, method: :post, path: "https://test.api.nylas.com/foo",
132+
payload: payload, api_key: "fake-key")
133+
134+
expect(request[:method]).to eq(:post)
135+
expect(request[:url]).to eq("https://test.api.nylas.com/foo")
136+
expect(request[:payload]).to eq("{}")
137+
expect(request[:headers]).to include("Content-type" => "application/json")
138+
end
139+
129140
it "returns the correct request with a json payload" do
130141
payload = { foo: "bar" }
131142
request = http_client.send(:build_request, method: :post, path: "https://test.api.nylas.com/foo",

0 commit comments

Comments
 (0)