Roundup Tracker - Issues

Issue 2551304

classification
Implement collision detection (lost edit prevention) for XML-RPC
Type: behavior Severity: normal
Components: API Versions:
process
Status: new
:
: : rouilj
Priority: :

Created on 2023-12-11 17:23 by rouilj, last changed 2026-08-03 05:32 by rouilj.

Messages
msg7872 Author: [hidden] (rouilj) Date: 2023-12-11 17:23
It looks like the XML-RPC endpoint doesn't have collision detection. This can lead to lost
edits.

Since xml-rpc is done over http, if-match and etag headers should work to prevent this.
However any existing clients will have to be updated to use it.

I don't see anything in the xmlrpc spec that is designed to prevent this.

Possible sequence that results in lost data:


  user 1 xmlrpc display issue1 status is open
  user 2 xmlrpc display issue1 status is open
  user 2 modifies status to closed via set call
  user 1 modifies status to new via set call

user2's changes are lost.
msg8488 Author: [hidden] (rouilj) Date: 2026-08-03 05:32
I wonder if there are enough users to make fixing this worthwhile.

Maybe it's time to deprecate xml-rpc endpoint since we have rest.

We need to send etag on display() and require if-match on set().

It looks like you have to subclass the transport. Some google AI gave me this to record
the headers and make them available. Note this will overwrite the headers on each call.
So threads will each need a unique instance of the class.

import xmlrpc.client

class HeaderCapturingTransport(xmlrpc.client.Transport):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # This will hold the response headers from the most recent request
        self.last_headers = None

    def single_request(self, host, handler, request_body, verbose=False):
        # 1. Establish the HTTP connection using standard internals
        connection = self.make_connection(host)
        try:
            if verbose:
                connection.set_debuglevel(1)

            # 2. Send request headers and content
            self.send_request(connection, handler, request_body, debug=verbose)
            self.send_content(connection, request_body)

            # 3. Intercept the HTTP response
            response = connection.getresponse()
            
            # 4. CAPTURE HEADERS: Parse and save them to the transport instance
            # response.headers is an http.client.HTTPMessage object
            self.last_headers = response.headers

            # 5. Handle the response payload normally using standard validation
            if response.status == 200:
                return self.parse_response(response)
            
            # Handle non-200 responses appropriately
            raise xmlrpc.client.ProtocolError(
                host + handler,
                response.status,
                response.reason,
                response.headers
            )
        finally:
            self.close()

# Initialize the custom transport
transport = HeaderCapturingTransport()

# Bind the transport to your ServerProxy
proxy = xmlrpc.client.ServerProxy('http://localhost:8000', transport=transport)

try:
    # Make your normal RPC call
    result = proxy.some_remote_function()
    print("RPC Result:", result)
    
    # Read the response headers captured during the call
    headers = transport.last_headers
    if headers:
        print("\n--- Response Headers ---")
        # Access a specific header safely (case-insensitive)
        print("Server Type:", headers.get("Server"))
        print("Content Type:", headers.get("Content-Type"))
        
        # Iterate over all headers
        for key, value in headers.items():
            print(f"{key}: {value}")

except xmlrpc.client.Error as err:
    print(f"XML-RPC Error: {err}")


Also update the xmlrpc docs. It still has some python2 constructs.
History
Date User Action Args
2026-08-03 05:32:44rouiljsetmessages: + msg8488
2023-12-11 17:23:35rouiljcreate