diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..f6c19d2
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,13 @@
+[run]
+source = zope.publisher
+omit =
+    */flycheck_*py
+
+[report]
+precision = 2
+exclude_lines =
+    pragma: no cover
+    if __name__ == '__main__':
+    raise NotImplementedError
+    self.fail
+    raise AssertionError
diff --git a/.gitignore b/.gitignore
index bb7ceb7..4047009 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,5 @@ dist
 *.pyc
 .dir-locals.el
 docs/_build
+.coverage
+htmlcov/
diff --git a/CHANGES.rst b/CHANGES.rst
index 05ed569..372f8d2 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -13,6 +13,10 @@
 
 - Drop support for Python 3.3.
 
+- Fix ``XMLRPCResponse`` having a str body (instead of a bytes body)
+  which could lead to ``TypeError`` on Python 3. See `issue 26
+  <https://github.com/zopefoundation/zope.publisher/issues/26>`_.
+
 
 4.3.2 (2017-05-23)
 ==================
diff --git a/MANIFEST.in b/MANIFEST.in
index a98d6ea..73e054e 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,8 @@
 include *.rst
 include *.txt
 include tox.ini
+include .travis.yml
+include .coveragerc
 include bootstrap.py
 include buildout.cfg
 
diff --git a/src/zope/publisher/xmlrpc.py b/src/zope/publisher/xmlrpc.py
index ed4bd9c..2f15c9b 100644
--- a/src/zope/publisher/xmlrpc.py
+++ b/src/zope/publisher/xmlrpc.py
@@ -121,6 +121,12 @@ class XMLRPCResponse(HTTPResponse):
                 self.handleException(sys.exc_info())
                 return
 
+        # HTTP response payloads are byte strings, and methods like
+        # consumeBody rely on that, but xmlrpc.client.dumps produces
+        # native strings, which is incorrect on Python 3.
+        if not isinstance(body, bytes):
+            body = body.encode('utf-8')
+
         headers = [('content-type', 'text/xml;charset=utf-8'),
                    ('content-length', str(len(body)))]
         self._headers.update(dict((k, [v]) for (k, v) in headers))
@@ -172,7 +178,7 @@ class XMLRPCResponse(HTTPResponse):
 
 @implementer(IXMLRPCView)
 class XMLRPCView(object):
-    """A base XML-RPC view that can be used as mix-in for XML-RPC views.""" 
+    """A base XML-RPC view that can be used as mix-in for XML-RPC views."""
 
     def __init__(self, context, request):
         self.context = context
diff --git a/tox.ini b/tox.ini
index 379bf98..850f194 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 envlist =
-    py27,py34,py35,py36,pypy,pypy3,docs
+    py27,py34,py35,py36,pypy,pypy3,docs,coverage
 
 [testenv]
 commands =
@@ -18,20 +18,15 @@ setenv =
     LC_CTYPE=en_US.UTF-8
 
 [testenv:coverage]
+usedevelop = true
 basepython =
-    python2.7
+    python3.6
 commands =
-#   The installed version messes up nose's test discovery / coverage reporting
-#   So, we uninstall that from the environment, and then install the editable
-#   version, before running nosetests.
-    pip uninstall -y zope.publisher
-    pip install -e .[test]
-    nosetests --with-xunit --with-xcoverage
+    coverage run -m zope.testrunner --test-path=src []
+    coverage report --fail-under=92
 deps =
-    nose
+    {[testenv]deps}
     coverage
-    nosexcover
-    .[test]
 
 
 [testenv:docs]

XMLRPCResponse.consumeBody produces `TypeError` on Python 3
Discovered when updating [zope.app.testing.functional](https://github.com/zopefoundation/zope.app.testing/blob/5208545dc52fb0f5699b608fd0cc17c5fb91d55e/src/zope/app/testing/functional.py#L89) for Python 3:
```python
            try:
                b = self._response.consumeBody()
            except TypeError:
                from zope.publisher.http import DirectResult
                from zope.publisher.xmlrpc import XMLRPCResponse
                if (isinstance(self._response, XMLRPCResponse)
                    and isinstance(getattr(self._response, '_result', None), DirectResult)):
                    # Somewhere in the publisher we're getting a DirectResult
                    # whose body is a sequence of strings, but we're expecting
                    # bytes
                    b = ''.join(self._response._result.body)
```

I've tracked that somewhere down to `XMLRPCResponse.setResult`.

[HTTPResponse.consumeBody](https://github.com/zopefoundation/zope.publisher/blob/master/src/zope/publisher/http.py#L805) simply does `b''.join(self._result)` When `self._result` is a `DirectResult` that will call its [`__iter__` method](https://github.com/zopefoundation/zope.publisher/blob/master/src/zope/publisher/http.py#L1072):
```python
    def __iter__(self):
        if isinstance(self.body, bytes):
            return iter([self.body])
        return iter(self.body)
```

In the case of an XMLRPCResponse, the `DirectResult` is instantiated with `(body,)`, where `body` is the result of [xmlrpc.client.dumps](https://github.com/zopefoundation/zope.publisher/blob/master/src/zope/publisher/xmlrpc.py#L111). This is incredibly [poorly documented on the web](https://docs.python.org/3/library/xmlrpc.client.html#xmlrpc.client.dumps), but [the source reveals](https://github.com/python/cpython/blob/3.6/Lib/xmlrpc/client.py#L998) that `dumps` returns a native string, i.e., unicode on Python 3.

So `consumeBody` winds up doing `b''.join(iter(u'some unicode'))` which as you can imagine fails.
diff --git a/src/zope/publisher/tests/test_xmlrpc.py b/src/zope/publisher/tests/test_xmlrpc.py
index 71030a1..12a54fe 100644
--- a/src/zope/publisher/tests/test_xmlrpc.py
+++ b/src/zope/publisher/tests/test_xmlrpc.py
@@ -13,18 +13,30 @@
 ##############################################################################
 """Testing the XML-RPC Publisher code.
 """
-import sys
 import doctest
+import unittest
+
 import zope.component.testing
 from zope.publisher import xmlrpc
 from zope.security.checker import defineChecker, Checker, CheckerPublic
 
-if sys.version_info[0] == 2:
+try:
     import xmlrpclib
-else:
+except ImportError:
     import xmlrpc.client as xmlrpclib
 
-def setUp(test):
+class TestXMLRPCResponse(unittest.TestCase):
+
+    def testConsumeBody(self):
+        response = xmlrpc.XMLRPCResponse()
+        response.setResult(['hi'])
+
+        body = response.consumeBody()
+        self.assertIsInstance(body, bytes)
+        self.assertIn(b'<methodResponse>', body)
+
+
+def doctest_setUp(test):
     zope.component.testing.setUp(test)
     zope.component.provideAdapter(xmlrpc.ListPreMarshaller)
     zope.component.provideAdapter(xmlrpc.TuplePreMarshaller)
@@ -45,18 +57,13 @@ def setUp(test):
                   Checker({'value':CheckerPublic}, {}))
 
 def test_suite():
-    return doctest.DocFileSuite(
-        "xmlrpc.txt", package="zope.publisher",
-        setUp=setUp, tearDown=zope.component.testing.tearDown,
-        optionflags=doctest.ELLIPSIS
-        )
-
-# Proper zope.component/zope.interface support requires PyPy 2.5.1+.
-# Older versions fail to hash types correctly. This manifests itself here
-# as being unable to find the marshlers registered as adapters for types
-# like 'list' and 'dict'. As of Jun 1 2015, Travis CI is still using PyPy 2.5.0.
-# All we can do is skip the test.
-if hasattr(sys, 'pypy_version_info') and sys.pypy_version_info[:3] == (2,5,0):
-    import unittest
-    def test_suite():
-        return unittest.TestSuite(())
+    return unittest.TestSuite((
+        unittest.defaultTestLoader.loadTestsFromName(__name__),
+        doctest.DocFileSuite(
+            "xmlrpc.txt",
+            package="zope.publisher",
+            setUp=doctest_setUp,
+            tearDown=zope.component.testing.tearDown,
+            optionflags=doctest.ELLIPSIS
+        ),
+    ))

pip install -e .[test]
parse_log_pytest
requirements.txt
pytest
3.6
doc-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
certifi==2021.5.30
importlib-metadata==4.8.3
iniconfig==1.1.1
packaging==21.3
pluggy==1.0.0
py==1.11.0
pyparsing==3.1.4
pytest==7.0.1
python-gettext==4.1
pytz==2025.2
six==1.17.0
swebench-matterhorn @ file:///swebench_matterhorn
tomli==1.2.3
typing_extensions==4.1.1
zipp==3.6.0
zope.browser==2.4
zope.component==5.1.0
zope.configuration==4.4.1
zope.contenttype==4.6
zope.deprecation==4.4.0
zope.event==4.6
zope.exceptions==4.6
zope.hookable==5.4
zope.i18n==4.9.0
zope.i18nmessageid==5.1.1
zope.interface==5.5.2
zope.location==4.3
zope.proxy==4.6.1
-e git+https://github.com/zopefoundation/zope.publisher.git@dc4f23e4b71835bdc858c338a1ca230cf4178feb#egg=zope.publisher
zope.schema==6.2.1
zope.security==5.8
zope.testing==5.0.1
zope.testrunner==5.6

name: zope.publisher
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - packaging==21.3
      - pluggy==1.0.0
      - py==1.11.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - python-gettext==4.1
      - pytz==2025.2
      - six==1.17.0
      - swebench-matterhorn==0.0.0
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - zipp==3.6.0
      - zope-browser==2.4
      - zope-component==5.1.0
      - zope-configuration==4.4.1
      - zope-contenttype==4.6
      - zope-deprecation==4.4.0
      - zope-event==4.6
      - zope-exceptions==4.6
      - zope-hookable==5.4
      - zope-i18n==4.9.0
      - zope-i18nmessageid==5.1.1
      - zope-interface==5.5.2
      - zope-location==4.3
      - zope-proxy==4.6.1
      - zope-schema==6.2.1
      - zope-security==5.8
      - zope-testing==5.0.1
      - zope-testrunner==5.6
prefix: /opt/conda/envs/zope.publisher

src/zope/publisher/tests/test_xmlrpc.py::TestXMLRPCResponse::testConsumeBody
src/zope/publisher/tests/test_xmlrpc.py::test_suite
MANIFEST.in .gitignore tox.ini .coveragerc CHANGES.rst src/zope/publisher/xmlrpc.py
MANIFEST.in .gitignore tox.ini .coveragerc CHANGES.rst src/zope/publisher/xmlrpc.py