From 3e22e8dcfadfd3284cab6427b9b5bf4d2fe81cb9 Mon Sep 17 00:00:00 2001 From: kristapsmezatucs Date: Wed, 16 Sep 2026 13:11:43 +0300 Subject: [PATCH 1/3] Reapply MTOM support on top of savon 2.17.5 --- lib/savon/builder.rb | 17 +++++++++++++++-- lib/savon/operation.rb | 22 +++++++++++++++------- lib/savon/options.rb | 8 +++++++- lib/savon/transport/logging.rb | 1 + 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/lib/savon/builder.rb b/lib/savon/builder.rb index 41e9e905..6a8dea0b 100644 --- a/lib/savon/builder.rb +++ b/lib/savon/builder.rb @@ -277,15 +277,28 @@ def build_multipart_message(message_xml) # the mail.body.encoded algorithm reorders the parts, default order is [ "text/plain", "text/enriched", "text/html" ] # should redefine the sort order, because the soap request xml should be the first - multipart_message.body.set_sort_order ["text/xml"] + multipart_message.body.set_sort_order ['application/xop+xml', 'text/xml'] multipart_message.body.encoded(multipart_message.content_transfer_encoding) end def init_multipart_message(message_xml) multipart_message = Mail.new + + # MTOM differs from general SOAP attachments: + # 1. binary encoding + # 2. application/xop+xml mime type + if @locals[:mtom] + type = "application/xop+xml; charset=#{@globals[:encoding]}; type=\"text/xml\"" + + multipart_message.transport_encoding = 'binary' + message_xml.force_encoding('BINARY') + else + type = 'text/xml' + end + xml_part = Mail::Part.new do - content_type 'text/xml' + content_type type body message_xml # in Content-Type the start parameter is recommended (RFC 2387) content_id '' diff --git a/lib/savon/operation.rb b/lib/savon/operation.rb index 6cd92c05..3cfc9ad0 100644 --- a/lib/savon/operation.rb +++ b/lib/savon/operation.rb @@ -133,14 +133,22 @@ def soap_headers(builder) if builder.multipart # RFC 2387 §3 (multipart/related) - SOAP envelope is the root body part - headers["Content-Type"] = [ - "multipart/related", - "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"", - "start=\"#{builder.multipart[:start]}\"", - "boundary=\"#{builder.multipart[:multipart_boundary]}\"" - ].join("; ") + content_type = ["multipart/related"] + + if @locals[:mtom] + # SOAP 1.2 MTOM §3.1 - the root part is an XOP package + content_type << "type=\"application/xop+xml\"" + content_type << "start-info=\"application/soap+xml\"" + else + content_type << "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"" + end + + content_type << "start=\"#{builder.multipart[:start]}\"" + content_type << "boundary=\"#{builder.multipart[:multipart_boundary]}\"" + + headers["Content-Type"] = content_type.join("; ") headers["MIME-Version"] = "1.0" - headers["Accept-Encoding"] = "gzip,deflate" + headers["Accept-Encoding"] = "gzip,deflate" unless @locals[:mtom] else headers["Content-Type"] = CONTENT_TYPE[@globals[:soap_version]] % @globals[:encoding] end diff --git a/lib/savon/options.rb b/lib/savon/options.rb index 8c8e854b..7f54a0cf 100644 --- a/lib/savon/options.rb +++ b/lib/savon/options.rb @@ -499,7 +499,8 @@ def initialize(options = {}) defaults = { advanced_typecasting: true, response_parser: :nokogiri, - multipart: false + multipart: false, + mtom: false } super defaults.merge(options) @@ -562,6 +563,11 @@ def attachments(attachments) @options[:attachments] = attachments end + # MTOM attachments https://www.w3.org/TR/soap12-mtom/ + def mtom(mtom) + @options[:mtom] = mtom + end + # Value of the SOAPAction HTTP header. def soap_action(soap_action) @options[:soap_action] = soap_action diff --git a/lib/savon/transport/logging.rb b/lib/savon/transport/logging.rb index 72b96804..377ab723 100644 --- a/lib/savon/transport/logging.rb +++ b/lib/savon/transport/logging.rb @@ -54,6 +54,7 @@ def headers_to_log(headers) def body_to_log(body) LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s + .force_encoding(@globals[:encoding]) end end end From 8a6233d5bceb8b4618861f94cbdb2943399e7b08 Mon Sep 17 00:00:00 2001 From: kristapsmezatucs Date: Thu, 1 Oct 2026 16:05:33 +0300 Subject: [PATCH 2/3] Fix lint issue --- lib/savon/builder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/savon/builder.rb b/lib/savon/builder.rb index 6a8dea0b..fafde477 100644 --- a/lib/savon/builder.rb +++ b/lib/savon/builder.rb @@ -17,7 +17,7 @@ module Savon # and signs the document when a WSSE signature is present. Message-body # serialization is delegated to {Savon::Message}, the Header element to # {Savon::Header}, and Hash-to-XML conversion to Gyoku. - class Builder + class Builder # rubocop:disable Metrics/ClassLength attr_reader :multipart SCHEMA_TYPES = { From a6a283e2a1c6a8a11cbbe27789bc889ce907a818 Mon Sep 17 00:00:00 2001 From: kristapsmezatucs Date: Fri, 2 Oct 2026 09:46:42 +0300 Subject: [PATCH 3/3] Refactor to avoid disabling cop for builder, minor improvement for logger --- lib/savon/builder.rb | 20 ++++++-------------- lib/savon/mtom.rb | 19 +++++++++++++++++++ lib/savon/transport/logging.rb | 3 +-- 3 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 lib/savon/mtom.rb diff --git a/lib/savon/builder.rb b/lib/savon/builder.rb index fafde477..ffe6f6e1 100644 --- a/lib/savon/builder.rb +++ b/lib/savon/builder.rb @@ -4,6 +4,7 @@ require "savon/header" require "savon/message" require "savon/effective_options" +require "savon/mtom" require "nokogiri" require "builder" require "gyoku" @@ -17,7 +18,9 @@ module Savon # and signs the document when a WSSE signature is present. Message-body # serialization is delegated to {Savon::Message}, the Header element to # {Savon::Header}, and Hash-to-XML conversion to Gyoku. - class Builder # rubocop:disable Metrics/ClassLength + class Builder + include Mtom + attr_reader :multipart SCHEMA_TYPES = { @@ -284,19 +287,8 @@ def build_multipart_message(message_xml) def init_multipart_message(message_xml) multipart_message = Mail.new - - # MTOM differs from general SOAP attachments: - # 1. binary encoding - # 2. application/xop+xml mime type - if @locals[:mtom] - type = "application/xop+xml; charset=#{@globals[:encoding]}; type=\"text/xml\"" - - multipart_message.transport_encoding = 'binary' - message_xml.force_encoding('BINARY') - else - type = 'text/xml' - end - + apply_mtom_encoding(multipart_message, message_xml) if @locals[:mtom] + type = xml_part_content_type xml_part = Mail::Part.new do content_type type body message_xml diff --git a/lib/savon/mtom.rb b/lib/savon/mtom.rb new file mode 100644 index 00000000..a918e47c --- /dev/null +++ b/lib/savon/mtom.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Savon + # MTOM request encoding (https://www.w3.org/TR/soap12-mtom/) for {Savon::Builder}. + module Mtom + private + + def apply_mtom_encoding(multipart_message, message_xml) + multipart_message.transport_encoding = 'binary' + message_xml.force_encoding('BINARY') + end + + def xml_part_content_type + return 'text/xml' unless @locals[:mtom] + + "application/xop+xml; charset=#{@globals[:encoding]}; type=\"text/xml\"" + end + end +end diff --git a/lib/savon/transport/logging.rb b/lib/savon/transport/logging.rb index 377ab723..eb673091 100644 --- a/lib/savon/transport/logging.rb +++ b/lib/savon/transport/logging.rb @@ -53,8 +53,7 @@ def headers_to_log(headers) end def body_to_log(body) - LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s - .force_encoding(@globals[:encoding]) + LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s.force_encoding(@globals[:encoding]) end end end