Skip to content

Conversation

herwinw
Copy link
Member

@herwinw herwinw commented Apr 23, 2025

This is a simple Rack application that acts as an XMLRPC server. This can be used in any Rack compatible webserver.

There are a few gems that provide a Rack-based XMLRPC server (rack-rpc, xmlrpc-rack_server and threez-rack-rpc), but none of these have been updated in the past 10 years.

Implementation has mostly been copied from ModRubyServer, tests have been inspired by the WebrickServer tests

require 'xmlrpc/parser'
require 'xmlrpc/utils'

module TestXMLRPC
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this because this gem is no longer a default gem. Our test names never conflict with others.

Suggested change
module TestXMLRPC
require 'xmlrpc/utils'

module TestXMLRPC
class Test_Rack < Test::Unit::TestCase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class Test_Rack < Test::Unit::TestCase
class TestRack < Test::Unit::TestCase
@@ -0,0 +1,79 @@
# coding: utf-8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this because it's the default in recent Ruby.

Suggested change
# coding: utf-8

s.add_introspection

return s
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return s
s
module TestXMLRPC
class Test_Rack < Test::Unit::TestCase
include Rack::Test::Methods
include XMLRPC::ParserWriterChooseMixin
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use XMLRPC::Create.new(XMLRPC::Config.default_writer.new) and XMLRPC::Config.default_parser.new instead?

Comment on lines 61 to 65
def assert_call_success(expect, methodname, *args)
request = create().methodCall(methodname, *args)
post("/", request, 'CONTENT_TYPE' => 'text/xml')
ok, param = parser().parseMethodResponse(last_response.body)
assert(ok)
assert_equal(expect, param)
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using call helper method like the following and assert_equal?

def call(method_name, *args) create = XMLRPC::Create.new(XMLRPC::Config.default_writer.new) request = create.methodCall(method_name, *args) post("/", request, "CONTENT_TYPE" => "text/xml") parser = XMLRPC::Config.default_parser.new parser.parseMethodResponse(last_response.body) end def test_rack assert_equal([true, 9], call("test.add", 4, 5)) assert_equal([false, XMLRPC::FaultException.new(1, "division by zero")], call("test.div", 1, 0)) # ... end
Comment on lines 41 to 60
def test_rack
# simple call
assert_call_success(9, "test.add", 4, 5)

# fault exception
assert_call_failure(1, "division by zero", "test.div", 1, 0)

# introspection
assert_call_success(["test.add", "test.div", "system.listMethods", "system.methodSignature", "system.methodHelp"], "system.listMethods")

# default handler (missing handler)
assert_call_failure(-99, "Method test.nonexisting missing or wrong number of parameters!", "test.nonexisting")

# default handler (wrong number of arguments)
assert_call_failure(-99, "Method test.add missing or wrong number of parameters!", "test.add", 1, 2, 3)

# multibyte characters
assert_call_success("あいうえおかきくけこ", 'test.add', "あいうえお", "かきくけこ")
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you create one test per test pattern?

For example:

def test_success assert_call_success(9, "test.add", 4, 5) end def test_exception assert_call_failure(1, "division by zero", "test.div", 1, 0) end
@herwinw herwinw force-pushed the rack branch 2 times, most recently from a7463e8 to c3f00ce Compare April 24, 2025 08:26
@herwinw
Copy link
Member Author

herwinw commented Apr 24, 2025

I think I addressed all the issues above. Most of them were to keep the code consistent with the other code/tests. I see this has had a very recent cleanup.



# Implements a XML-RPC server, which works with Rack
class RackServer < BasicServer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, Rack server means Puma, Passenger and so on.

Suggested change
class RackServer < BasicServer
class RackApplication < BasicServer
end

def test_successful_call
assert_equal([true, 9], call("test.add", 4, 5))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you align arguments for easy to read?

Suggested change
assert_equal([true, 9], call("test.add", 4, 5))
assert_equal([true, 9],
call("test.add", 4, 5))

request = create.methodCall(methodname, *args)
post("/", request, 'CONTENT_TYPE' => 'text/xml')
assert(last_response.ok?)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use power assert for easy to debug? BTW, when does ok? return false?

Suggested change
assert(last_response.ok?)
assert do
last_response.ok?
end
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/rack/rack/blob/main/lib/rack/response.rb#L189

ok? checks the HTTP status code for 200. This is mostly relevant for the exceptions, where XMLRPC will still return a HTTP OK (200) status, as opposed to using the HTTP status to signal an error like most REST-based APIs do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This results in output like this:

 | | | false #<Rack::MockResponse:0x00007fbc70b25aa0 @original_headers={"Content-type"=>"text/xml; charset=utf-8"}, @errors="", @status=201, @headers={"content-type"=>"text/xml; charset=utf-8", "content-length"=>"296"}, @writer=#<Method: Rack::MockResponse(Rack::Response::Helpers)#append(chunk) /home/herwin/xmlrpc/vendor/bundle/ruby/3.1.0/gems/rack-3.1.13/lib/rack/response.rb:359>, @block=nil, @body=["<?xml version=\"1.0\" ?><methodResponse><fault><value><struct><member><name>faultCode</name><value><i4>-99</i4></value></member><member><name>faultString</name><value><string>Method test.add missing or wrong number of parameters!</string></value></member></struct></value></fault></methodResponse>\n"], @buffered=true, @length=296, @cookies={}> /home/herwin/xmlrpc/test/test_rack_server.rb:73:in `call' /home/herwin/xmlrpc/test/test_rack_server.rb:59:in `test_wrong_number_of_arguments' 56: 57: def test_wrong_number_of_arguments 58: assert_equal([false, XMLRPC::FaultException.new(-99, "Method test.add missing or wrong number of parameters!")], => 59: call("test.add", 1, 2, 3)) 60: end 

I don't think this is the most readable output, I think the better option would be something like:

assert(last_response.ok?, "Expected HTTP status code 200, got #{last_response.status} instead")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. It's also better than assert(last_response.ok?).

end

def test_multibyte_characters
assert_equal([true, "あいうえおかきくけこ"], call('test.add', "あいうえお", "かきくけこ"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert_equal([true, "あいうえおかきくけこ"], call('test.add', "あいうえお", "かきくけこ"))
assert_equal([true, "あいうえおかきくけこ"], call("test.add", "あいうえお", "かきくけこ"))
Comment on lines +560 to +570
msg = <<-"MSGEND"
<html>
<head>
<title>#{err}</title>
</head>
<body>
<h1>#{err}</h1>
<p>Unexpected error occurred while processing XML-RPC request!</p>
</body>
</html>
MSGEND
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
msg = <<-"MSGEND"
<html>
<head>
<title>#{err}</title>
</head>
<body>
<h1>#{err}</h1>
<p>Unexpected error occurred while processing XML-RPC request!</p>
</body>
</html>
MSGEND
msg = <<-HTML
<html>
<head>
<title>#{err}</title>
</head>
<body>
<h1>#{err}</h1>
<p>Unexpected error occurred while processing XML-RPC request!</p>
</body>
</html>
HTML
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've got this exact same code snippet two more times in the existing code (CGIServer and ModRubyServer). I would prefer to keep this the same for now, and as a future work extract it to some shared code and remove this duplication. It's easier to find all the occurrences if we keep this exactly the same for now.
Once the code has been deduplicated, this improvement can be applied.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Let's use the approach.

return http_error(400, "Bad Request") unless parse_content_type(env['CONTENT_TYPE']).first == "text/xml"
return http_error(411, "Length Required") unless length > 0

req = Rack::Request.new(env)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to require rack in initialize?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would guess this is already available when you run this as Rack middleware, so that should only be required if you run this rack app without rack. This seems like an unlikely scenario to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Let's revisit this when we receive a bug report / feature request about this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, this is a Rack application not Rack middleware, right?

This is a simple Rack middleware that acts as an XMLRPC server. This can be used in any Rack compatible webserver.
@herwinw herwinw mentioned this pull request Apr 24, 2025
@kou kou changed the title Add XMLRPC::RackServer Add XMLRPC::RackApplication Apr 24, 2025
@kou kou merged commit 6bf6eb4 into ruby:master Apr 24, 2025
27 checks passed
@kou
Copy link
Member

kou commented Apr 24, 2025

Thanks.

@herwinw herwinw deleted the rack branch April 25, 2025 05:38
@herwinw herwinw mentioned this pull request Apr 25, 2025
kou pushed a commit that referenced this pull request Apr 25, 2025
And add the missing Rack application to the docs (#56) Closes #57 --------- Co-authored-by: Herwin <herwinw@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants