From be29d808a28b9274de39c4e8b8a0448f5bf3682d Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Mon, 23 Feb 2026 14:39:01 -0500 Subject: [PATCH 1/2] 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) --- CHANGELOG.md | 4 + lib/nylas/handler/api_operations.rb | 85 ++++++++++--------- lib/nylas/resources/bookings.rb | 6 +- spec/nylas/handler/api_operations_spec.rb | 76 +++++++++++++++-- .../handler/http_client_integration_spec.rb | 40 +++++++++ spec/nylas/handler/http_client_spec.rb | 11 +++ spec/nylas/resources/bookings_spec.rb | 19 ++++- 7 files changed, 191 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40f9b805..cdd604a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Unreleased +* 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) +* Added support for request_body parameter on DELETE (e.g. cancellation_reason for bookings) (#536) + ### [6.7.0] * Added access to response headers diff --git a/lib/nylas/handler/api_operations.rb b/lib/nylas/handler/api_operations.rb index b1d59560..6c5f275b 100644 --- a/lib/nylas/handler/api_operations.rb +++ b/lib/nylas/handler/api_operations.rb @@ -58,19 +58,20 @@ module Post protected include HttpClient - # Performs a POST call to the Nylas API. - # - # @param path [String] Destination path for the call. - # @param query_params [Hash, {}] Query params to pass to the call. - # @param request_body [Hash, nil] Request body to pass to the call. - # @param headers [Hash, {}] Additional HTTP headers to include in the payload. + # Performs a POST call to the Nylas API. + # + # @param path [String] Destination path for the call. + # @param query_params [Hash, {}] Query params to pass to the call. + # @param request_body [Hash, nil] Request body to pass to the call. + # Defaults to {} when nil to ensure Content-Type: application/json is sent. + # @param headers [Hash, {}] Additional HTTP headers to include in the payload. # @return [Array(Hash, String, Hash)] Nylas data object, API Request ID, and response headers. def post(path:, query_params: {}, request_body: nil, headers: {}) response = execute( method: :post, path: path, query: query_params, - payload: request_body, + payload: request_body || {}, headers: headers, api_key: api_key, timeout: timeout @@ -85,19 +86,20 @@ module Put protected include HttpClient - # Performs a PUT call to the Nylas API. - # - # @param path [String] Destination path for the call. - # @param query_params [Hash, {}] Query params to pass to the call. - # @param request_body [Hash, nil] Request body to pass to the call. - # @param headers [Hash, {}] Additional HTTP headers to include in the payload. + # Performs a PUT call to the Nylas API. + # + # @param path [String] Destination path for the call. + # @param query_params [Hash, {}] Query params to pass to the call. + # @param request_body [Hash, nil] Request body to pass to the call. + # Defaults to {} when nil to ensure Content-Type: application/json is sent. + # @param headers [Hash, {}] Additional HTTP headers to include in the payload. # @return Nylas data object and API Request ID. def put(path:, query_params: {}, request_body: nil, headers: {}) response = execute( method: :put, path: path, query: query_params, - payload: request_body, + payload: request_body || {}, headers: headers, api_key: api_key, timeout: timeout @@ -112,19 +114,20 @@ module Patch protected include HttpClient - # Performs a PATCH call to the Nylas API. - # - # @param path [String] Destination path for the call. - # @param query_params [Hash, {}] Query params to pass to the call. - # @param request_body [Hash, nil] Request body to pass to the call. - # @param headers [Hash, {}] Additional HTTP headers to include in the payload. + # Performs a PATCH call to the Nylas API. + # + # @param path [String] Destination path for the call. + # @param query_params [Hash, {}] Query params to pass to the call. + # @param request_body [Hash, nil] Request body to pass to the call. + # Defaults to {} when nil to ensure Content-Type: application/json is sent. + # @param headers [Hash, {}] Additional HTTP headers to include in the payload. # @return Nylas data object and API Request ID. def patch(path:, query_params: {}, request_body: nil, headers: {}) response = execute( method: :patch, path: path, query: query_params, - payload: request_body, + payload: request_body || {}, headers: headers, api_key: api_key, timeout: timeout @@ -139,25 +142,27 @@ module Delete protected include HttpClient - # Performs a DELETE call to the Nylas API. - # - # @param path [String] Destination path for the call. - # @param query_params [Hash, {}] Query params to pass to the call. - # @param headers [Hash, {}] Additional HTTP headers to include in the payload. - # @return Nylas data object and API Request ID. - def delete(path:, query_params: {}, headers: {}) - response = execute( - method: :delete, - path: path, - query: query_params, - headers: headers, - payload: nil, - api_key: api_key, - timeout: timeout - ) - - [response[:data], response[:request_id]] - end + # Performs a DELETE call to the Nylas API. + # + # @param path [String] Destination path for the call. + # @param query_params [Hash, {}] Query params to pass to the call. + # @param request_body [Hash, nil] Optional request body (e.g. cancellation_reason for bookings). + # Defaults to {} to ensure Content-Type: application/json is sent. + # @param headers [Hash, {}] Additional HTTP headers to include in the payload. + # @return Nylas data object and API Request ID. + def delete(path:, query_params: {}, request_body: nil, headers: {}) + response = execute( + method: :delete, + path: path, + query: query_params, + headers: headers, + payload: request_body || {}, + api_key: api_key, + timeout: timeout + ) + + [response[:data], response[:request_id]] + end end end end diff --git a/lib/nylas/resources/bookings.rb b/lib/nylas/resources/bookings.rb index e357fc73..62406381 100644 --- a/lib/nylas/resources/bookings.rb +++ b/lib/nylas/resources/bookings.rb @@ -64,11 +64,13 @@ def confirm(booking_id:, request_body:, query_params: nil) # Delete a booking. # @param booking_id [String] The id of the booking to delete. # @param query_params [Hash, nil] Query params to pass to the request. + # @param request_body [Hash, nil] Optional body params (e.g. cancellation_reason). # @return [Array(TrueClass, String)] True and the API Request ID for the delete operation. - def destroy(booking_id:, query_params: nil) + def destroy(booking_id:, query_params: nil, request_body: nil) _, request_id = delete( path: "#{api_uri}/v3/scheduling/bookings/#{booking_id}", - query_params: query_params + query_params: query_params, + request_body: request_body ) [true, request_id] diff --git a/spec/nylas/handler/api_operations_spec.rb b/spec/nylas/handler/api_operations_spec.rb index df6c5739..bb12dfe9 100644 --- a/spec/nylas/handler/api_operations_spec.rb +++ b/spec/nylas/handler/api_operations_spec.rb @@ -143,7 +143,7 @@ def initialize(api_key, api_uri, timeout) method: :post, path: path, query: {}, - payload: nil, + payload: {}, headers: {}, api_key: api_key, timeout: timeout @@ -164,7 +164,7 @@ def initialize(api_key, api_uri, timeout) method: :post, path: path, query: {}, - payload: nil, + payload: {}, headers: {}, api_key: api_key, timeout: timeout @@ -174,6 +174,22 @@ def initialize(api_key, api_uri, timeout) expect(response).to eq([mock_response[:data], mock_response[:request_id], nil]) end + + it "passes request_body to execute when provided" do + path = "#{api_uri}/path" + request_body = { foo: "bar" } + allow(api_operations).to receive(:execute).with( + method: :post, + path: path, + query: {}, + payload: request_body, + headers: {}, + api_key: api_key, + timeout: timeout + ).and_return(mock_response) + + api_operations.send(:post, path: path, request_body: request_body) + end end end @@ -206,7 +222,7 @@ def initialize(api_key, api_uri, timeout) method: :put, path: path, query: {}, - payload: nil, + payload: {}, headers: {}, api_key: api_key, timeout: timeout @@ -216,6 +232,22 @@ def initialize(api_key, api_uri, timeout) expect(response).to eq([mock_response[:data], mock_response[:request_id]]) end + + it "passes request_body to execute when provided" do + path = "#{api_uri}/path" + request_body = { foo: "bar" } + allow(api_operations).to receive(:execute).with( + method: :put, + path: path, + query: {}, + payload: request_body, + headers: {}, + api_key: api_key, + timeout: timeout + ).and_return(mock_response) + + api_operations.send(:put, path: path, request_body: request_body) + end end end @@ -248,7 +280,7 @@ def initialize(api_key, api_uri, timeout) method: :patch, path: path, query: {}, - payload: nil, + payload: {}, headers: {}, api_key: api_key, timeout: timeout @@ -258,6 +290,22 @@ def initialize(api_key, api_uri, timeout) expect(response).to eq([mock_response[:data], mock_response[:request_id]]) end + + it "passes request_body to execute when provided" do + path = "#{api_uri}/path" + request_body = { foo: "bar" } + allow(api_operations).to receive(:execute).with( + method: :patch, + path: path, + query: {}, + payload: request_body, + headers: {}, + api_key: api_key, + timeout: timeout + ).and_return(mock_response) + + api_operations.send(:patch, path: path, request_body: request_body) + end end end @@ -271,7 +319,7 @@ def initialize(api_key, api_uri, timeout) method: :delete, path: path, query: query_params, - payload: nil, + payload: {}, headers: headers, api_key: api_key, timeout: timeout @@ -288,7 +336,7 @@ def initialize(api_key, api_uri, timeout) method: :delete, path: path, query: {}, - payload: nil, + payload: {}, headers: {}, api_key: api_key, timeout: timeout @@ -298,6 +346,22 @@ def initialize(api_key, api_uri, timeout) expect(response).to eq([mock_response[:data], mock_response[:request_id]]) end + + it "passes request_body to execute when provided" do + path = "#{api_uri}/path" + request_body = { cancellation_reason: "Meeting cancelled" } + allow(api_operations).to receive(:execute).with( + method: :delete, + path: path, + query: {}, + payload: request_body, + headers: {}, + api_key: api_key, + timeout: timeout + ).and_return(mock_response) + + api_operations.send(:delete, path: path, request_body: request_body) + end end end end diff --git a/spec/nylas/handler/http_client_integration_spec.rb b/spec/nylas/handler/http_client_integration_spec.rb index d26be56a..7fea2420 100644 --- a/spec/nylas/handler/http_client_integration_spec.rb +++ b/spec/nylas/handler/http_client_integration_spec.rb @@ -75,6 +75,46 @@ class TestHttpClientIntegration end end + describe "Integration Tests - Content-Type for body-less requests (HTTParty fix)" do + it "sends Content-Type: application/json for POST with empty payload" do + stub_request(:post, "https://test.api.nylas.com/v3/connect/revoke") + .with( + body: "{}", + headers: { "Content-Type" => "application/json" } + ) + .to_return(status: 200, body: "{}", headers: { "Content-Type" => "application/json" }) + + http_client.send(:execute, + method: :post, + path: "https://test.api.nylas.com/v3/connect/revoke", + timeout: 30, + payload: {}, + api_key: "fake-key") + + expect(WebMock).to have_requested(:post, "https://test.api.nylas.com/v3/connect/revoke") + .with(headers: { "Content-Type" => "application/json" }, body: "{}") + end + + it "sends Content-Type: application/json for DELETE with empty payload" do + stub_request(:delete, "https://test.api.nylas.com/v3/scheduling/bookings/booking-123") + .with( + body: "{}", + headers: { "Content-Type" => "application/json" } + ) + .to_return(status: 200, body: "{}", headers: { "Content-Type" => "application/json" }) + + http_client.send(:execute, + method: :delete, + path: "https://test.api.nylas.com/v3/scheduling/bookings/booking-123", + timeout: 30, + payload: {}, + api_key: "fake-key") + + expect(WebMock).to have_requested(:delete, "https://test.api.nylas.com/v3/scheduling/bookings/booking-123") + .with(headers: { "Content-Type" => "application/json" }, body: "{}") + end + end + describe "Integration Tests - backwards compatibility" do it "maintains the same response format as rest-client" do response_json = { "data" => { "id" => "123", "name" => "test" } } diff --git a/spec/nylas/handler/http_client_spec.rb b/spec/nylas/handler/http_client_spec.rb index a5d79ff8..9edb9c23 100644 --- a/spec/nylas/handler/http_client_spec.rb +++ b/spec/nylas/handler/http_client_spec.rb @@ -126,6 +126,17 @@ class TestHttpClient end context "when building request with a payload" do + it "returns the correct request with empty json payload and sets Content-Type" do + payload = {} + request = http_client.send(:build_request, method: :post, path: "https://test.api.nylas.com/foo", + payload: payload, api_key: "fake-key") + + expect(request[:method]).to eq(:post) + expect(request[:url]).to eq("https://test.api.nylas.com/foo") + expect(request[:payload]).to eq("{}") + expect(request[:headers]).to include("Content-type" => "application/json") + end + it "returns the correct request with a json payload" do payload = { foo: "bar" } request = http_client.send(:build_request, method: :post, path: "https://test.api.nylas.com/foo", diff --git a/spec/nylas/resources/bookings_spec.rb b/spec/nylas/resources/bookings_spec.rb index 79311258..5205a3d2 100644 --- a/spec/nylas/resources/bookings_spec.rb +++ b/spec/nylas/resources/bookings_spec.rb @@ -185,7 +185,7 @@ booking_id = "booking-123" path = "#{api_uri}/v3/scheduling/bookings/#{booking_id}" allow(bookings).to receive(:delete) - .with(path: path, query_params: nil) + .with(path: path, query_params: nil, request_body: nil) .and_return(delete_response) bookings_response = bookings.destroy(booking_id: booking_id) @@ -197,11 +197,26 @@ query_params = { "foo": "bar" } path = "#{api_uri}/v3/scheduling/bookings/#{booking_id}" allow(bookings).to receive(:delete) - .with(path: path, query_params: query_params) + .with(path: path, query_params: query_params, request_body: nil) .and_return(delete_response) bookings_response = bookings.destroy(booking_id: booking_id, query_params: query_params) expect(bookings_response).to eq(delete_response) end + + it "calls the delete method with request_body for cancellation_reason" do + booking_id = "booking-123" + request_body = { cancellation_reason: "Meeting no longer needed" } + path = "#{api_uri}/v3/scheduling/bookings/#{booking_id}" + allow(bookings).to receive(:delete) + .with(path: path, query_params: nil, request_body: request_body) + .and_return(delete_response) + + bookings_response = bookings.destroy( + booking_id: booking_id, + request_body: request_body + ) + expect(bookings_response).to eq(delete_response) + end end end From 52e7b40d8fa22e7e0fe0fd5f4305bfd3ac9f77a2 Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Mon, 23 Feb 2026 14:47:10 -0500 Subject: [PATCH 2/2] Refactor API operation method documentation for clarity - Updated comments for POST, PUT, PATCH, and DELETE methods to improve readability and maintain consistency in parameter descriptions. - Ensured that the documentation accurately reflects the behavior of each method, including default values for request bodies and additional headers. --- lib/nylas/handler/api_operations.rb | 84 ++++++++++++++--------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/lib/nylas/handler/api_operations.rb b/lib/nylas/handler/api_operations.rb index 6c5f275b..873ba423 100644 --- a/lib/nylas/handler/api_operations.rb +++ b/lib/nylas/handler/api_operations.rb @@ -58,13 +58,13 @@ module Post protected include HttpClient - # Performs a POST call to the Nylas API. - # - # @param path [String] Destination path for the call. - # @param query_params [Hash, {}] Query params to pass to the call. - # @param request_body [Hash, nil] Request body to pass to the call. - # Defaults to {} when nil to ensure Content-Type: application/json is sent. - # @param headers [Hash, {}] Additional HTTP headers to include in the payload. + # Performs a POST call to the Nylas API. + # + # @param path [String] Destination path for the call. + # @param query_params [Hash, {}] Query params to pass to the call. + # @param request_body [Hash, nil] Request body to pass to the call. + # Defaults to {} when nil to ensure Content-Type: application/json is sent. + # @param headers [Hash, {}] Additional HTTP headers to include in the payload. # @return [Array(Hash, String, Hash)] Nylas data object, API Request ID, and response headers. def post(path:, query_params: {}, request_body: nil, headers: {}) response = execute( @@ -86,13 +86,13 @@ module Put protected include HttpClient - # Performs a PUT call to the Nylas API. - # - # @param path [String] Destination path for the call. - # @param query_params [Hash, {}] Query params to pass to the call. - # @param request_body [Hash, nil] Request body to pass to the call. - # Defaults to {} when nil to ensure Content-Type: application/json is sent. - # @param headers [Hash, {}] Additional HTTP headers to include in the payload. + # Performs a PUT call to the Nylas API. + # + # @param path [String] Destination path for the call. + # @param query_params [Hash, {}] Query params to pass to the call. + # @param request_body [Hash, nil] Request body to pass to the call. + # Defaults to {} when nil to ensure Content-Type: application/json is sent. + # @param headers [Hash, {}] Additional HTTP headers to include in the payload. # @return Nylas data object and API Request ID. def put(path:, query_params: {}, request_body: nil, headers: {}) response = execute( @@ -114,13 +114,13 @@ module Patch protected include HttpClient - # Performs a PATCH call to the Nylas API. - # - # @param path [String] Destination path for the call. - # @param query_params [Hash, {}] Query params to pass to the call. - # @param request_body [Hash, nil] Request body to pass to the call. - # Defaults to {} when nil to ensure Content-Type: application/json is sent. - # @param headers [Hash, {}] Additional HTTP headers to include in the payload. + # Performs a PATCH call to the Nylas API. + # + # @param path [String] Destination path for the call. + # @param query_params [Hash, {}] Query params to pass to the call. + # @param request_body [Hash, nil] Request body to pass to the call. + # Defaults to {} when nil to ensure Content-Type: application/json is sent. + # @param headers [Hash, {}] Additional HTTP headers to include in the payload. # @return Nylas data object and API Request ID. def patch(path:, query_params: {}, request_body: nil, headers: {}) response = execute( @@ -142,27 +142,27 @@ module Delete protected include HttpClient - # Performs a DELETE call to the Nylas API. - # - # @param path [String] Destination path for the call. - # @param query_params [Hash, {}] Query params to pass to the call. - # @param request_body [Hash, nil] Optional request body (e.g. cancellation_reason for bookings). - # Defaults to {} to ensure Content-Type: application/json is sent. - # @param headers [Hash, {}] Additional HTTP headers to include in the payload. - # @return Nylas data object and API Request ID. - def delete(path:, query_params: {}, request_body: nil, headers: {}) - response = execute( - method: :delete, - path: path, - query: query_params, - headers: headers, - payload: request_body || {}, - api_key: api_key, - timeout: timeout - ) - - [response[:data], response[:request_id]] - end + # Performs a DELETE call to the Nylas API. + # + # @param path [String] Destination path for the call. + # @param query_params [Hash, {}] Query params to pass to the call. + # @param request_body [Hash, nil] Optional request body (e.g. cancellation_reason for bookings). + # Defaults to {} to ensure Content-Type: application/json is sent. + # @param headers [Hash, {}] Additional HTTP headers to include in the payload. + # @return Nylas data object and API Request ID. + def delete(path:, query_params: {}, request_body: nil, headers: {}) + response = execute( + method: :delete, + path: path, + query: query_params, + headers: headers, + payload: request_body || {}, + api_key: api_key, + timeout: timeout + ) + + [response[:data], response[:request_id]] + end end end end