From 7b0e4d7df7d97cc7d660cba9782a182d9df636ec Mon Sep 17 00:00:00 2001 From: Sean Millichamp Date: Fri, 21 Mar 2025 10:45:42 -0400 Subject: [PATCH] Add body_as_json configuration parameter Allow the user to configure the client to use a Content-Type of application/json for all requests with a body unless the request explicitly provides its own Content-Type header. Using application/json provides a broader range of compatibility with the GitLab API, especially for calls where the body expects more complex data structures. This defaults to false for backwards compatibility. --- lib/gitlab/configuration.rb | 3 ++- lib/gitlab/request.rb | 23 ++++++++++++++++++++++- spec/gitlab/request_spec.rb | 30 ++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/configuration.rb b/lib/gitlab/configuration.rb index bd7d7b45d..b9d682cf7 100644 --- a/lib/gitlab/configuration.rb +++ b/lib/gitlab/configuration.rb @@ -5,7 +5,7 @@ module Gitlab # Defines constants and methods related to configuration. module Configuration # An array of valid keys in the options hash when configuring a Gitlab::API. - VALID_OPTIONS_KEYS = %i[endpoint private_token user_agent sudo httparty pat_prefix].freeze + VALID_OPTIONS_KEYS = %i[endpoint private_token user_agent sudo httparty pat_prefix body_as_json].freeze # The user agent that will be sent to the API endpoint if none is set. DEFAULT_USER_AGENT = "Gitlab Ruby Gem #{Gitlab::VERSION}" @@ -41,6 +41,7 @@ def reset self.httparty = get_httparty_config(ENV['GITLAB_API_HTTPARTY_OPTIONS']) self.sudo = nil self.user_agent = DEFAULT_USER_AGENT + self.body_as_json = false end private diff --git a/lib/gitlab/request.rb b/lib/gitlab/request.rb index f1a01a30e..d899b118f 100644 --- a/lib/gitlab/request.rb +++ b/lib/gitlab/request.rb @@ -12,7 +12,7 @@ class Request headers 'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded' parser(proc { |body, _| parse(body) }) - attr_accessor :private_token, :endpoint, :pat_prefix + attr_accessor :private_token, :endpoint, :pat_prefix, :body_as_json # Converts the response body to an ObjectifiedHash. def self.parse(body) @@ -49,6 +49,8 @@ def self.decode(response) params[:headers].merge!(authorization_header) end + jsonify_body_content(params) if body_as_json + retries_left = params[:ratelimit_retries] || 3 begin response = self.class.send(method, endpoint + path, params) @@ -114,5 +116,24 @@ def authorization_header def httparty_config(options) options.merge!(httparty) if httparty end + + # Handle 'body_as_json' configuration option + # Modifies passed params in place. + def jsonify_body_content(params) + # Only modify the content type if there is a body to process AND multipath + # was not explicitly requested. There are no uses of multipart in this code + # today, but file upload methods require it and someone might be manually + # crafting a post call with it: + return unless params[:body] && params[:multipart] != true + + # If the caller explicitly requested a Content-Type during the call, assume + # they know best and have formatted the body as required: + return if params[:headers]&.key?('Content-Type') + + # If we make it here, then we assume it is safe to JSON encode the body: + params[:headers] ||= {} + params[:headers]['Content-Type'] = 'application/json' + params[:body] = params[:body].to_json + end end end diff --git a/spec/gitlab/request_spec.rb b/spec/gitlab/request_spec.rb index 8f3b2a25d..de971708d 100644 --- a/spec/gitlab/request_spec.rb +++ b/spec/gitlab/request_spec.rb @@ -15,6 +15,7 @@ it { is_expected.to respond_to :post } it { is_expected.to respond_to :put } it { is_expected.to respond_to :delete } + it { is_expected.to respond_to :patch } describe '.default_options' do it 'has default values' do @@ -288,4 +289,33 @@ ).to have_been_made end end + + describe 'JSON request bodies' do + subject(:request) { described_class.new } + + let(:endpoint) { 'https://example.com/api/v4' } + let(:path) { '/testpath' } + let(:request_path) { "#{endpoint}#{path}" } + + before do + request.private_token = 'token' + request.endpoint = endpoint + request.body_as_json = true + allow(request).to receive(:httparty) # rubocop:disable RSpec/SubjectStub + + stub_request(:post, request_path).with(headers: { 'Content-Type' => 'application/json' }) + end + + it 'makes a request with application/json encoding when no Content-Type is specified' do + request.post(path, body: { param: 'val' }) + + expect(a_request(:post, request_path).with(body: { param: 'val' }.to_json)).to have_been_made + end + + it 'passes the unmodified request when a Content-Type is specified' do + request.post(path, body: { param: 'val' }.to_json, headers: { 'Content-Type' => 'application/json' }) + + expect(a_request(:post, request_path).with(body: { param: 'val' }.to_json)).to have_been_made + end + end end