Index: tracrpc/tests/xml_rpc.py
===================================================================
--- tracrpc/tests/xml_rpc.py	(revision 13728)
+++ tracrpc/tests/xml_rpc.py	(working copy)
@@ -114,31 +114,35 @@
         # Enable ticket manipulator
         plugin = os.path.join(rpc_testenv.tracdir, 'plugins',
                               'InvalidXmlCharHandler.py')
-        open(plugin, 'w').write(
-        "from trac.core import *\n"
-        "from tracrpc.api import IXMLRPCHandler\n"
-        "class UniChr(Component):\n"
-        "    implements(IXMLRPCHandler)\n"
-        "    def xmlrpc_namespace(self):\n"
-        "        return 'test_unichr'\n"
-        "    def xmlrpc_methods(self):\n"
-        "        yield ('XML_RPC', ((str, int),), self.unichr)\n"
-        "    def unichr(self, req, code):\n"
-        "        return unichr(code)\n")
+        open(plugin, 'w').write(r"""
+from trac.core import *
+from tracrpc.api import IXMLRPCHandler
+class UniChr(Component):
+    implements(IXMLRPCHandler)
+    def xmlrpc_namespace(self):
+        return 'test_unichr'
+    def xmlrpc_methods(self):
+        yield ('XML_RPC', ((str, int),), self.unichr)
+    def unichr(self, req, code):
+        return (r'\U%08x' % code).decode('unicode-escape')
+""")
         rpc_testenv.restart()
 
         from tracrpc.xml_rpc import _illegal_unichrs, REPLACEMENT_CHAR
 
         num_checked = 0
         for low, high in _illegal_unichrs:
-            if low < sys.maxunicode:
-                for x in (low, (low + high) / 2, high):
-                    self.assertEquals(REPLACEMENT_CHAR,
-                                      self.user.test_unichr.unichr(x),
-                                      "Failed unichr with %d" % (x,))
-                    num_checked += 1
+            for x in (low, (low + high) / 2, high):
+                self.assertEquals(REPLACEMENT_CHAR,
+                                  self.user.test_unichr.unichr(x),
+                                  "Failed unichr with U+%04X" % (x,))
+                num_checked += 1
         self.assertTrue(num_checked, "No invalid XML characters checked.")
 
+        # surrogate pair on narrow build
+        self.assertEquals(u'\U0001D4C1',
+                          self.user.test_unichr.unichr(0x1D4C1))
+
         # Remove plugin and restart
         os.unlink(plugin)
         rpc_testenv.restart()
Index: tracrpc/xml_rpc.py
===================================================================
--- tracrpc/xml_rpc.py	(revision 13728)
+++ tracrpc/xml_rpc.py	(working copy)
@@ -33,20 +33,20 @@
 
 REPLACEMENT_CHAR = u'\uFFFD' # Unicode replacement character
 
-_illegal_unichrs = [ (0x00, 0x08), (0x0B, 0x1F), (0x7F, 0x84), (0x86, 0x9F),
-                     (0xD800, 0xDFFF), (0xFDD0, 0xFDDF), (0xFFFE, 0xFFFF),
-                     (0x1FFFE, 0x1FFFF), (0x2FFFE, 0x2FFFF), 
-                     (0x3FFFE, 0x3FFFF), (0x4FFFE, 0x4FFFF),
-                     (0x5FFFE, 0x5FFFF), (0x6FFFE, 0x6FFFF),
-                     (0x7FFFE, 0x7FFFF), (0x8FFFE, 0x8FFFF),
-                     (0x9FFFE, 0x9FFFF), (0xAFFFE, 0xAFFFF),
-                     (0xBFFFE, 0xBFFFF), (0xCFFFE, 0xCFFFF),
-                     (0xDFFFE, 0xDFFFF), (0xEFFFE, 0xEFFFF),
-                     (0xFFFFE, 0xFFFFF), (0x10FFFE, 0x10FFFF) ]
+_illegal_unichrs = [(0x00, 0x08), (0x0B, 0x1F), (0x7F, 0x84), (0x86, 0x9F),
+                    (0xFDD0, 0xFDDF), (0xFFFE, 0xFFFF)]
+if sys.maxunicode >= 0x10000:  # not narrow build
+    _illegal_unichrs.extend([(0x1FFFE, 0x1FFFF), (0x2FFFE, 0x2FFFF),
+                             (0x3FFFE, 0x3FFFF), (0x4FFFE, 0x4FFFF),
+                             (0x5FFFE, 0x5FFFF), (0x6FFFE, 0x6FFFF),
+                             (0x7FFFE, 0x7FFFF), (0x8FFFE, 0x8FFFF),
+                             (0x9FFFE, 0x9FFFF), (0xAFFFE, 0xAFFFF),
+                             (0xBFFFE, 0xBFFFF), (0xCFFFE, 0xCFFFF),
+                             (0xDFFFE, 0xDFFFF), (0xEFFFE, 0xEFFFF),
+                             (0xFFFFE, 0xFFFFF), (0x10FFFE, 0x10FFFF)])
 
-_illegal_ranges = ["%s-%s" % (unichr(low), unichr(high)) 
-                   for (low, high) in _illegal_unichrs 
-                                if low < sys.maxunicode]
+_illegal_ranges = ["%s-%s" % (unichr(low), unichr(high))
+                   for (low, high) in _illegal_unichrs]
 
 _illegal_xml_chars_RE = re.compile(u'[%s]' % u''.join(_illegal_ranges))
 
