D2089: wireproto: introduce type for raw byte responses (API)

2018-02-12 Thread indygreg (Gregory Szorc)
This revision was automatically updated to reflect the committed changes.
Closed by commit rHG2f7290555c96: wireproto: introduce type for raw byte 
responses (API) (authored by indygreg, committed by ).

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D2089?vs=5352&id=5534

REVISION DETAIL
  https://phab.mercurial-scm.org/D2089

AFFECTED FILES
  hgext/largefiles/proto.py
  mercurial/wireproto.py
  mercurial/wireprotoserver.py
  mercurial/wireprototypes.py
  tests/sshprotoext.py
  tests/test-wireproto.py

CHANGE DETAILS

diff --git a/tests/test-wireproto.py b/tests/test-wireproto.py
--- a/tests/test-wireproto.py
+++ b/tests/test-wireproto.py
@@ -1,8 +1,10 @@
 from __future__ import absolute_import, print_function
 
 from mercurial import (
+error,
 util,
 wireproto,
+wireprototypes,
 )
 stringio = util.stringio
 
@@ -42,7 +44,13 @@
 return ['batch']
 
 def _call(self, cmd, **args):
-return wireproto.dispatch(self.serverrepo, proto(args), cmd)
+res = wireproto.dispatch(self.serverrepo, proto(args), cmd)
+if isinstance(res, wireprototypes.bytesresponse):
+return res.data
+elif isinstance(res, bytes):
+return res
+else:
+raise error.Abort('dummy client does not support response type')
 
 def _callstream(self, cmd, **args):
 return stringio(self._call(cmd, **args))
diff --git a/tests/sshprotoext.py b/tests/sshprotoext.py
--- a/tests/sshprotoext.py
+++ b/tests/sshprotoext.py
@@ -49,7 +49,7 @@
 l = self._fin.readline()
 assert l == b'between\n'
 rsp = wireproto.dispatch(self._repo, self._proto, b'between')
-wireprotoserver._sshv1respondbytes(self._fout, rsp)
+wireprotoserver._sshv1respondbytes(self._fout, rsp.data)
 
 super(prehelloserver, self).serve_forever()
 
@@ -74,7 +74,7 @@
 # Send the upgrade response.
 self._fout.write(b'upgraded %s %s\n' % (token, name))
 servercaps = wireproto.capabilities(self._repo, self._proto)
-rsp = b'capabilities: %s' % servercaps
+rsp = b'capabilities: %s' % servercaps.data
 self._fout.write(b'%d\n' % len(rsp))
 self._fout.write(rsp)
 self._fout.write(b'\n')
diff --git a/mercurial/wireprototypes.py b/mercurial/wireprototypes.py
--- a/mercurial/wireprototypes.py
+++ b/mercurial/wireprototypes.py
@@ -5,6 +5,11 @@
 
 from __future__ import absolute_import
 
+class bytesresponse(object):
+"""A wire protocol response consisting of raw bytes."""
+def __init__(self, data):
+self.data = data
+
 class ooberror(object):
 """wireproto reply: failure of a batch of operation
 
diff --git a/mercurial/wireprotoserver.py b/mercurial/wireprotoserver.py
--- a/mercurial/wireprotoserver.py
+++ b/mercurial/wireprotoserver.py
@@ -274,6 +274,9 @@
 if isinstance(rsp, bytes):
 req.respond(HTTP_OK, HGTYPE, body=rsp)
 return []
+elif isinstance(rsp, wireprototypes.bytesresponse):
+req.respond(HTTP_OK, HGTYPE, body=rsp.data)
+return []
 elif isinstance(rsp, wireprototypes.streamreslegacy):
 gen = rsp.gen
 req.respond(HTTP_OK, HGTYPE)
@@ -435,6 +438,8 @@
 
 if isinstance(rsp, bytes):
 _sshv1respondbytes(self._fout, rsp)
+elif isinstance(rsp, wireprototypes.bytesresponse):
+_sshv1respondbytes(self._fout, rsp.data)
 elif isinstance(rsp, wireprototypes.streamres):
 _sshv1respondstream(self._fout, rsp)
 elif isinstance(rsp, wireprototypes.streamreslegacy):
diff --git a/mercurial/wireproto.py b/mercurial/wireproto.py
--- a/mercurial/wireproto.py
+++ b/mercurial/wireproto.py
@@ -37,6 +37,7 @@
 urlerr = util.urlerr
 urlreq = util.urlreq
 
+bytesresponse = wireprototypes.bytesresponse
 ooberror = wireprototypes.ooberror
 pushres = wireprototypes.pushres
 pusherr = wireprototypes.pusherr
@@ -696,16 +697,24 @@
 result = func(repo, proto)
 if isinstance(result, ooberror):
 return result
+
+# For now, all batchable commands must return bytesresponse or
+# raw bytes (for backwards compatibility).
+assert isinstance(result, (bytesresponse, bytes))
+if isinstance(result, bytesresponse):
+result = result.data
 res.append(escapearg(result))
-return ';'.join(res)
+
+return bytesresponse(';'.join(res))
 
 @wireprotocommand('between', 'pairs')
 def between(repo, proto, pairs):
 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
 r = []
 for b in repo.between(pairs):
 r.append(encodelist(b) + "\n")
-return "".join(r)
+
+return bytesresponse(''.join(r))
 
 @wireprotocommand('branchmap')
 def branchmap(repo, proto):
@@ -715,15 +724,17 @@
 branchname = urlreq.quote(encoding.fromlocal(branch))
 branchnodes = encodelist(nodes)
 heads.append('%s %s' % (b

D2089: wireproto: introduce type for raw byte responses (API)

2018-02-07 Thread indygreg (Gregory Szorc)
indygreg updated this revision to Diff 5352.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D2089?vs=5341&id=5352

REVISION DETAIL
  https://phab.mercurial-scm.org/D2089

AFFECTED FILES
  hgext/largefiles/proto.py
  mercurial/wireproto.py
  mercurial/wireprotoserver.py
  mercurial/wireprototypes.py
  tests/sshprotoext.py
  tests/test-wireproto.py

CHANGE DETAILS

diff --git a/tests/test-wireproto.py b/tests/test-wireproto.py
--- a/tests/test-wireproto.py
+++ b/tests/test-wireproto.py
@@ -1,8 +1,10 @@
 from __future__ import absolute_import, print_function
 
 from mercurial import (
+error,
 util,
 wireproto,
+wireprototypes,
 )
 stringio = util.stringio
 
@@ -42,7 +44,13 @@
 return ['batch']
 
 def _call(self, cmd, **args):
-return wireproto.dispatch(self.serverrepo, proto(args), cmd)
+res = wireproto.dispatch(self.serverrepo, proto(args), cmd)
+if isinstance(res, wireprototypes.bytesresponse):
+return res.data
+elif isinstance(res, bytes):
+return res
+else:
+raise error.Abort('dummy client does not support response type')
 
 def _callstream(self, cmd, **args):
 return stringio(self._call(cmd, **args))
diff --git a/tests/sshprotoext.py b/tests/sshprotoext.py
--- a/tests/sshprotoext.py
+++ b/tests/sshprotoext.py
@@ -49,7 +49,7 @@
 l = self._fin.readline()
 assert l == b'between\n'
 rsp = wireproto.dispatch(self._repo, self._proto, b'between')
-wireprotoserver._sshv1respondbytes(self._fout, rsp)
+wireprotoserver._sshv1respondbytes(self._fout, rsp.data)
 
 super(prehelloserver, self).serve_forever()
 
@@ -74,7 +74,7 @@
 # Send the upgrade response.
 self._fout.write(b'upgraded %s %s\n' % (token, name))
 servercaps = wireproto.capabilities(self._repo, self._proto)
-rsp = b'capabilities: %s' % servercaps
+rsp = b'capabilities: %s' % servercaps.data
 self._fout.write(b'%d\n' % len(rsp))
 self._fout.write(rsp)
 self._fout.write(b'\n')
diff --git a/mercurial/wireprototypes.py b/mercurial/wireprototypes.py
--- a/mercurial/wireprototypes.py
+++ b/mercurial/wireprototypes.py
@@ -5,6 +5,11 @@
 
 from __future__ import absolute_import
 
+class bytesresponse(object):
+"""A wire protocol response consisting of raw bytes."""
+def __init__(self, data):
+self.data = data
+
 class ooberror(object):
 """wireproto reply: failure of a batch of operation
 
diff --git a/mercurial/wireprotoserver.py b/mercurial/wireprotoserver.py
--- a/mercurial/wireprotoserver.py
+++ b/mercurial/wireprotoserver.py
@@ -274,6 +274,9 @@
 if isinstance(rsp, bytes):
 req.respond(HTTP_OK, HGTYPE, body=rsp)
 return []
+elif isinstance(rsp, wireprototypes.bytesresponse):
+req.respond(HTTP_OK, HGTYPE, body=rsp.data)
+return []
 elif isinstance(rsp, wireprototypes.streamreslegacy):
 gen = rsp.gen
 req.respond(HTTP_OK, HGTYPE)
@@ -435,6 +438,8 @@
 
 if isinstance(rsp, bytes):
 _sshv1respondbytes(self._fout, rsp)
+elif isinstance(rsp, wireprototypes.bytesresponse):
+_sshv1respondbytes(self._fout, rsp.data)
 elif isinstance(rsp, wireprototypes.streamres):
 _sshv1respondstream(self._fout, rsp)
 elif isinstance(rsp, wireprototypes.streamreslegacy):
diff --git a/mercurial/wireproto.py b/mercurial/wireproto.py
--- a/mercurial/wireproto.py
+++ b/mercurial/wireproto.py
@@ -37,6 +37,7 @@
 urlerr = util.urlerr
 urlreq = util.urlreq
 
+bytesresponse = wireprototypes.bytesresponse
 ooberror = wireprototypes.ooberror
 pushres = wireprototypes.pushres
 pusherr = wireprototypes.pusherr
@@ -696,16 +697,24 @@
 result = func(repo, proto)
 if isinstance(result, ooberror):
 return result
+
+# For now, all batchable commands must return bytesresponse or
+# raw bytes (for backwards compatibility).
+assert isinstance(result, (bytesresponse, bytes))
+if isinstance(result, bytesresponse):
+result = result.data
 res.append(escapearg(result))
-return ';'.join(res)
+
+return bytesresponse(';'.join(res))
 
 @wireprotocommand('between', 'pairs')
 def between(repo, proto, pairs):
 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
 r = []
 for b in repo.between(pairs):
 r.append(encodelist(b) + "\n")
-return "".join(r)
+
+return bytesresponse(''.join(r))
 
 @wireprotocommand('branchmap')
 def branchmap(repo, proto):
@@ -715,15 +724,17 @@
 branchname = urlreq.quote(encoding.fromlocal(branch))
 branchnodes = encodelist(nodes)
 heads.append('%s %s' % (branchname, branchnodes))
-return '\n'.join(heads)
+
+return bytesresponse('\n'.join(heads))
 
 @wireprotocommand('branches', 'nodes')
 def branches(re

D2089: wireproto: introduce type for raw byte responses (API)

2018-02-07 Thread indygreg (Gregory Szorc)
indygreg updated this revision to Diff 5341.
indygreg edited the summary of this revision.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D2089?vs=5325&id=5341

REVISION DETAIL
  https://phab.mercurial-scm.org/D2089

AFFECTED FILES
  hgext/largefiles/proto.py
  mercurial/wireproto.py
  mercurial/wireprotoserver.py
  mercurial/wireprototypes.py
  tests/sshprotoext.py
  tests/test-wireproto.py

CHANGE DETAILS

diff --git a/tests/test-wireproto.py b/tests/test-wireproto.py
--- a/tests/test-wireproto.py
+++ b/tests/test-wireproto.py
@@ -1,8 +1,10 @@
 from __future__ import absolute_import, print_function
 
 from mercurial import (
+error,
 util,
 wireproto,
+wireprototypes,
 )
 stringio = util.stringio
 
@@ -42,7 +44,13 @@
 return ['batch']
 
 def _call(self, cmd, **args):
-return wireproto.dispatch(self.serverrepo, proto(args), cmd)
+res = wireproto.dispatch(self.serverrepo, proto(args), cmd)
+if isinstance(res, wireprototypes.bytesresponse):
+return res.data
+elif isinstance(res, bytes):
+return res
+else:
+raise error.Abort('dummy client does not support response type')
 
 def _callstream(self, cmd, **args):
 return stringio(self._call(cmd, **args))
diff --git a/tests/sshprotoext.py b/tests/sshprotoext.py
--- a/tests/sshprotoext.py
+++ b/tests/sshprotoext.py
@@ -49,7 +49,7 @@
 l = self._fin.readline()
 assert l == b'between\n'
 rsp = wireproto.dispatch(self._repo, self._proto, b'between')
-wireprotoserver._sshv1respondbytes(self._fout, rsp)
+wireprotoserver._sshv1respondbytes(self._fout, rsp.data)
 
 super(prehelloserver, self).serve_forever()
 
@@ -74,7 +74,7 @@
 # Send the upgrade response.
 self._fout.write(b'upgraded %s %s\n' % (token, name))
 servercaps = wireproto.capabilities(self._repo, self._proto)
-rsp = b'capabilities: %s' % servercaps
+rsp = b'capabilities: %s' % servercaps.data
 self._fout.write(b'%d\n' % len(rsp))
 self._fout.write(rsp)
 self._fout.write(b'\n')
diff --git a/mercurial/wireprototypes.py b/mercurial/wireprototypes.py
--- a/mercurial/wireprototypes.py
+++ b/mercurial/wireprototypes.py
@@ -5,6 +5,11 @@
 
 from __future__ import absolute_import
 
+class bytesresponse(object):
+"""A wire protocol response consisting of raw bytes."""
+def __init__(self, data):
+self.data = data
+
 class ooberror(object):
 """wireproto reply: failure of a batch of operation
 
diff --git a/mercurial/wireprotoserver.py b/mercurial/wireprotoserver.py
--- a/mercurial/wireprotoserver.py
+++ b/mercurial/wireprotoserver.py
@@ -274,6 +274,9 @@
 if isinstance(rsp, bytes):
 req.respond(HTTP_OK, HGTYPE, body=rsp)
 return []
+elif isinstance(rsp, wireprototypes.bytesresponse):
+req.respond(HTTP_OK, HGTYPE, body=rsp.data)
+return []
 elif isinstance(rsp, wireprototypes.streamreslegacy):
 gen = rsp.gen
 req.respond(HTTP_OK, HGTYPE)
@@ -435,6 +438,8 @@
 
 if isinstance(rsp, bytes):
 _sshv1respondbytes(self._fout, rsp)
+elif isinstance(rsp, wireprototypes.bytesresponse):
+_sshv1respondbytes(self._fout, rsp.data)
 elif isinstance(rsp, wireprototypes.streamres):
 _sshv1respondstream(self._fout, rsp)
 elif isinstance(rsp, wireprototypes.streamreslegacy):
diff --git a/mercurial/wireproto.py b/mercurial/wireproto.py
--- a/mercurial/wireproto.py
+++ b/mercurial/wireproto.py
@@ -37,6 +37,7 @@
 urlerr = util.urlerr
 urlreq = util.urlreq
 
+bytesresponse = wireprototypes.bytesresponse
 ooberror = wireprototypes.ooberror
 pushres = wireprototypes.pushres
 pusherr = wireprototypes.pusherr
@@ -696,16 +697,24 @@
 result = func(repo, proto)
 if isinstance(result, ooberror):
 return result
+
+# For now, all batchable commands must return bytesresponse or
+# raw bytes (for backwards compatibility).
+assert isinstance(result, (bytesresponse, bytes))
+if isinstance(result, bytesresponse):
+result = result.data
 res.append(escapearg(result))
-return ';'.join(res)
+
+return bytesresponse(';'.join(res))
 
 @wireprotocommand('between', 'pairs')
 def between(repo, proto, pairs):
 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
 r = []
 for b in repo.between(pairs):
 r.append(encodelist(b) + "\n")
-return "".join(r)
+
+return bytesresponse(''.join(r))
 
 @wireprotocommand('branchmap')
 def branchmap(repo, proto):
@@ -715,15 +724,17 @@
 branchname = urlreq.quote(encoding.fromlocal(branch))
 branchnodes = encodelist(nodes)
 heads.append('%s %s' % (branchname, branchnodes))
-return '\n'.join(heads)
+
+return bytesresponse('\n'.join(heads))
 
 @wireprot

D2089: wireproto: introduce type for raw byte responses (API)

2018-02-07 Thread indygreg (Gregory Szorc)
indygreg updated this revision to Diff 5325.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D2089?vs=5323&id=5325

REVISION DETAIL
  https://phab.mercurial-scm.org/D2089

AFFECTED FILES
  hgext/largefiles/proto.py
  mercurial/wireproto.py
  mercurial/wireprotoserver.py
  mercurial/wireprototypes.py
  tests/sshprotoext.py
  tests/test-wireproto.py

CHANGE DETAILS

diff --git a/tests/test-wireproto.py b/tests/test-wireproto.py
--- a/tests/test-wireproto.py
+++ b/tests/test-wireproto.py
@@ -1,8 +1,10 @@
 from __future__ import absolute_import, print_function
 
 from mercurial import (
+error,
 util,
 wireproto,
+wireprototypes,
 )
 stringio = util.stringio
 
@@ -42,7 +44,13 @@
 return ['batch']
 
 def _call(self, cmd, **args):
-return wireproto.dispatch(self.serverrepo, proto(args), cmd)
+res = wireproto.dispatch(self.serverrepo, proto(args), cmd)
+if isinstance(res, wireprototypes.bytesresponse):
+return res.data
+elif isinstance(res, bytes):
+return res
+else:
+raise error.Abort('dummy client does not support response type')
 
 def _callstream(self, cmd, **args):
 return stringio(self._call(cmd, **args))
diff --git a/tests/sshprotoext.py b/tests/sshprotoext.py
--- a/tests/sshprotoext.py
+++ b/tests/sshprotoext.py
@@ -74,7 +74,7 @@
 # Send the upgrade response.
 self._fout.write(b'upgraded %s %s\n' % (token, name))
 servercaps = wireproto.capabilities(self._repo, self._proto)
-rsp = b'capabilities: %s' % servercaps
+rsp = b'capabilities: %s' % servercaps.data
 self._fout.write(b'%d\n' % len(rsp))
 self._fout.write(rsp)
 self._fout.write(b'\n')
diff --git a/mercurial/wireprototypes.py b/mercurial/wireprototypes.py
--- a/mercurial/wireprototypes.py
+++ b/mercurial/wireprototypes.py
@@ -5,6 +5,11 @@
 
 from __future__ import absolute_import
 
+class bytesresponse(object):
+"""A wire protocol response consisting of raw bytes."""
+def __init__(self, data):
+self.data = data
+
 class ooberror(object):
 """wireproto reply: failure of a batch of operation
 
diff --git a/mercurial/wireprotoserver.py b/mercurial/wireprotoserver.py
--- a/mercurial/wireprotoserver.py
+++ b/mercurial/wireprotoserver.py
@@ -274,6 +274,9 @@
 if isinstance(rsp, bytes):
 req.respond(HTTP_OK, HGTYPE, body=rsp)
 return []
+elif isinstance(rsp, wireprototypes.bytesresponse):
+req.respond(HTTP_OK, HGTYPE, body=rsp.data)
+return []
 elif isinstance(rsp, wireprototypes.streamreslegacy):
 gen = rsp.gen
 req.respond(HTTP_OK, HGTYPE)
@@ -389,6 +392,9 @@
 self._fout.write(v)
 self._fout.flush()
 
+def _sendbytes(self, v):
+self._sendresponse(v.data)
+
 def _sendstream(self, source):
 write = self._fout.write
 for chunk in source.gen:
@@ -409,7 +415,8 @@
 self._fout.flush()
 
 _handlers = {
-str: _sendresponse,
+bytes: _sendresponse,
+wireprototypes.bytesresponse: _sendbytes,
 wireprototypes.streamres: _sendstream,
 wireprototypes.streamreslegacy: _sendstream,
 wireprototypes.pushres: _sendpushresponse,
diff --git a/mercurial/wireproto.py b/mercurial/wireproto.py
--- a/mercurial/wireproto.py
+++ b/mercurial/wireproto.py
@@ -37,6 +37,7 @@
 urlerr = util.urlerr
 urlreq = util.urlreq
 
+bytesresponse = wireprototypes.bytesresponse
 ooberror = wireprototypes.ooberror
 pushres = wireprototypes.pushres
 pusherr = wireprototypes.pusherr
@@ -696,16 +697,24 @@
 result = func(repo, proto)
 if isinstance(result, ooberror):
 return result
+
+# For now, all batchable commands must return bytesresponse or
+# raw bytes (for backwards compatibility).
+assert isinstance(result, (bytesresponse, bytes))
+if isinstance(result, bytesresponse):
+result = result.data
 res.append(escapearg(result))
-return ';'.join(res)
+
+return bytesresponse(';'.join(res))
 
 @wireprotocommand('between', 'pairs')
 def between(repo, proto, pairs):
 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
 r = []
 for b in repo.between(pairs):
 r.append(encodelist(b) + "\n")
-return "".join(r)
+
+return bytesresponse(''.join(r))
 
 @wireprotocommand('branchmap')
 def branchmap(repo, proto):
@@ -715,15 +724,17 @@
 branchname = urlreq.quote(encoding.fromlocal(branch))
 branchnodes = encodelist(nodes)
 heads.append('%s %s' % (branchname, branchnodes))
-return '\n'.join(heads)
+
+return bytesresponse('\n'.join(heads))
 
 @wireprotocommand('branches', 'nodes')
 def branches(repo, proto, nodes):
 nodes = decodelist(nodes)
 r = []
 for b in repo.branches(nodes):
 r.append(encodelist(b) + "\n")
-return "".join(r)
+
+return b

D2089: wireproto: introduce type for raw byte responses (API)

2018-02-07 Thread indygreg (Gregory Szorc)
indygreg created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  Right now we simply return a str/bytes instance for simple
  responses. I want all wire protocol response types to be strongly
  typed. So let's invent and use a type for raw bytes responses.
  
  While I was here, I also switched a `str` to `bytes` in the
  ssh protocol handler. That should make Python 3 a bit happier.
  
  .. api::
  
Wire protocol command handlers now return a
wireprototypes.bytesresponse instead of a raw bytes instance.
Protocol handlers will continue handling bytes instances. However,
any extensions wrapping wire protocol commands will need to handle
the new type.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D2089

AFFECTED FILES
  hgext/largefiles/proto.py
  mercurial/wireproto.py
  mercurial/wireprotoserver.py
  mercurial/wireprototypes.py
  tests/sshprotoext.py
  tests/test-wireproto.py

CHANGE DETAILS

diff --git a/tests/test-wireproto.py b/tests/test-wireproto.py
--- a/tests/test-wireproto.py
+++ b/tests/test-wireproto.py
@@ -1,8 +1,10 @@
 from __future__ import absolute_import, print_function
 
 from mercurial import (
+error,
 util,
 wireproto,
+wireprototypes,
 )
 stringio = util.stringio
 
@@ -42,7 +44,13 @@
 return ['batch']
 
 def _call(self, cmd, **args):
-return wireproto.dispatch(self.serverrepo, proto(args), cmd)
+res = wireproto.dispatch(self.serverrepo, proto(args), cmd)
+if isinstance(res, wireprototypes.bytesresponse):
+return res.data
+elif isinstance(res, bytes):
+return res
+else:
+raise error.Abort('dummy client does not support response type')
 
 def _callstream(self, cmd, **args):
 return stringio(self._call(cmd, **args))
diff --git a/tests/sshprotoext.py b/tests/sshprotoext.py
--- a/tests/sshprotoext.py
+++ b/tests/sshprotoext.py
@@ -74,7 +74,7 @@
 # Send the upgrade response.
 self._fout.write(b'upgraded %s %s\n' % (token, name))
 servercaps = wireproto.capabilities(self._repo, self._proto)
-rsp = b'capabilities: %s' % servercaps
+rsp = b'capabilities: %s' % servercaps.data
 self._fout.write(b'%d\n' % len(rsp))
 self._fout.write(rsp)
 self._fout.write(b'\n')
diff --git a/mercurial/wireprototypes.py b/mercurial/wireprototypes.py
--- a/mercurial/wireprototypes.py
+++ b/mercurial/wireprototypes.py
@@ -5,6 +5,11 @@
 
 from __future__ import absolute_import
 
+class bytesresponse(object):
+"""A wire protocol response consisting of raw bytes."""
+def __init__(self, data):
+self.data = data
+
 class ooberror(object):
 """wireproto reply: failure of a batch of operation
 
diff --git a/mercurial/wireprotoserver.py b/mercurial/wireprotoserver.py
--- a/mercurial/wireprotoserver.py
+++ b/mercurial/wireprotoserver.py
@@ -274,6 +274,9 @@
 if isinstance(rsp, bytes):
 req.respond(HTTP_OK, HGTYPE, body=rsp)
 return []
+elif isinstance(rsp, wireprototypes.bytesresponse):
+req.respond(HTTP_OK, HGTYPE, body=rsp.data)
+return []
 elif isinstance(rsp, wireprototypes.streamres_legacy):
 gen = rsp.gen
 req.respond(HTTP_OK, HGTYPE)
@@ -389,6 +392,9 @@
 self._fout.write(v)
 self._fout.flush()
 
+def _sendbytes(self, v):
+self._sendresponse(v.data)
+
 def _sendstream(self, source):
 write = self._fout.write
 for chunk in source.gen:
@@ -409,7 +415,8 @@
 self._fout.flush()
 
 _handlers = {
-str: _sendresponse,
+bytes: _sendresponse,
+wireprototypes.bytesresponse: _sendbytes,
 wireprototypes.streamres: _sendstream,
 wireprototypes.streamres_legacy: _sendstream,
 wireprototypes.pushres: _sendpushresponse,
diff --git a/mercurial/wireproto.py b/mercurial/wireproto.py
--- a/mercurial/wireproto.py
+++ b/mercurial/wireproto.py
@@ -37,6 +37,7 @@
 urlerr = util.urlerr
 urlreq = util.urlreq
 
+bytesresponse = wireprototypes.bytesresponse
 ooberror = wireprototypes.ooberror
 pushres = wireprototypes.pushres
 pusherr = wireprototypes.pusherr
@@ -696,16 +697,24 @@
 result = func(repo, proto)
 if isinstance(result, ooberror):
 return result
+
+# For now, all batchable commands must return bytesresponse or
+# raw bytes (for backwards compatibility).
+assert isinstance(result, (bytesresponse, bytes))
+if isinstance(result, bytesresponse):
+result = result.data
 res.append(escapearg(result))
-return ';'.join(res)
+
+return bytesresponse(';'.join(res))
 
 @wireprotocommand('between', 'pairs')
 def between(repo, proto, pairs):
 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
 r = []
 for b in repo.between(pairs):
 r.append(encodelist(b) + "\n