diff --git a/mercurial/configitems.py b/mercurial/configitems.py --- a/mercurial/configitems.py +++ b/mercurial/configitems.py @@ -583,6 +583,9 @@ coreconfigitem('experimental', 'web.api.http-v2', default=False, ) +coreconfigitem('experimental', 'web.api.debugreflect', + default=False, +) coreconfigitem('experimental', 'xdiff', default=False, ) diff --git a/mercurial/util.py b/mercurial/util.py --- a/mercurial/util.py +++ b/mercurial/util.py @@ -2559,6 +2559,14 @@ return data + def readinto(self, b): + res = self.read(len(b)) + if res is None: + return None + + b[0:len(res)] = res + return len(res) + def stringmatcher(pattern, casesensitive=True): """ accepts a string, possibly starting with 're:' or 'literal:' prefix. diff --git a/mercurial/wireprotoframing.py b/mercurial/wireprotoframing.py --- a/mercurial/wireprotoframing.py +++ b/mercurial/wireprotoframing.py @@ -13,6 +13,12 @@ import struct +from .i18n import _ +from . import ( + error, + util, +) + FRAME_HEADER_SIZE = 4 FRAME_TYPE_COMMAND_NAME = 0x01 @@ -72,6 +78,41 @@ return frame +def readframe(fh): + """Read a unified framing protocol frame from a file object. + + Returns a 3-tuple of (type, flags, payload) for the decoded frame or + None if no frame is available. May raise if a malformed frame is + seen. + """ + header = bytearray(FRAME_HEADER_SIZE) + + readcount = fh.readinto(header) + + if readcount == 0: + return None + + if readcount != FRAME_HEADER_SIZE: + raise error.Abort(_('received incomplete frame: got %d bytes: %s') % + (readcount, header)) + + # 24 bits payload length (little endian) + # 4 bits frame type + # 4 bits frame flags + # ... payload + framelength = header[0] + 256 * header[1] + 16384 * header[2] + typeflags = header[3] + + frametype = (typeflags & 0xf0) >> 4 + frameflags = typeflags & 0x0f + + payload = fh.read(framelength) + if len(payload) != framelength: + raise error.Abort(_('frame length error: expected %d; got %d') % + (framelength, len(payload))) + + return frametype, frameflags, payload + def createcommandframes(cmd, args, datafh=None): """Create frames necessary to transmit a request to run a command. @@ -120,3 +161,178 @@ if done: break + +class serverreactor(object): + """Holds state of a server handling frame-based protocol requests. + + This class is the "brain" of the unified frame-based protocol server + component. While the protocol is stateless from the perspective of + requests/commands, something needs to track which frames have been + received, what frames to expect, etc. This class is that thing. + + Instances are modeled as a state machine of sorts. Instances are also + reactionary to external events. The point of this class is to encapsulate + the state of the connection and the exchange of frames, not to perform + work. Instead, callers tell this class when something occurs, like a + frame arriving. If that activity is worthy of a follow-up action (say + *run a command*), the return value of that handler will say so. + + I/O and CPU intensive operations are purposefully delegated outside of + this class. + """ + + def __init__(self, ui, repo): + self._ui = ui + self._repo = repo + self._state = 'idle' + self._activecommand = None + self._activeargs = None + self._activedata = None + self._expectingargs = None + self._expectingdata = None + self._activeargname = None + self._activeargchunks = None + + def onframerecv(self, frametype, frameflags, payload): + """Process a frame that has been received off the wire. + + Returns a dict with an ``action`` key that details what action, + if any, the consumer should take next. + """ + handlers = { + 'idle': self._onframeidle, + 'command-receiving-args': self._onframereceivingargs, + 'command-receiving-data': self._onframereceivingdata, + 'errored': self._onframeerrored, + } + + meth = handlers.get(self._state) + if not meth: + raise error.ProgrammingError('unhandled state: %s' % self._state) + + return meth(frametype, frameflags, payload) + + def _makeerrorresult(self, msg): + return { + 'action': 'error', + 'message': msg, + } + + def _makeruncommandresult(self): + return { + 'action': 'runcommand', + 'command': self._activecommand, + 'args': self._activeargs, + 'data': self._activedata.getvalue() if self._activedata else None, + } + + def _makewantframeresult(self): + return { + 'action': 'wantframe', + 'state': self._state, + } + + def _onframeidle(self, frametype, frameflags, payload): + # The only frame type that should be received in this state is a + # command request. + if frametype != FRAME_TYPE_COMMAND_NAME: + self._state = 'errored' + return self._makeerrorresult( + _('expected command frame; got %d') % frametype) + + self._activecommand = payload + self._activeargs = {} + self._activedata = None + + if frameflags & FLAG_COMMAND_NAME_EOS: + return self._makeruncommandresult() + + self._expectingargs = bool(frameflags & FLAG_COMMAND_NAME_HAVE_ARGS) + self._expectingdata = bool(frameflags & FLAG_COMMAND_NAME_HAVE_DATA) + + if self._expectingargs: + self._state = 'command-receiving-args' + return self._makewantframeresult() + elif self._expectingdata: + self._activedata = util.stringio() + self._state = 'command-receiving-data' + return self._makewantframeresult() + else: + self._state = 'errored' + return self._makeerrorresult(_('missing frame flags on ' + 'command frame')) + + def _onframereceivingargs(self, frametype, frameflags, payload): + if frametype != FRAME_TYPE_COMMAND_ARGUMENT: + self._state = 'errored' + return self._makeerrorresult(_('expected command argument ' + 'frame; got %d') % frametype) + + offset = 0 + namesize, valuesize = ARGUMENT_FRAME_HEADER.unpack_from(payload) + offset += ARGUMENT_FRAME_HEADER.size + + # The argument name MUST fit inside the frame. + argname = payload[offset:offset + namesize] + offset += namesize + + if len(argname) != namesize: + self._state = 'errored' + return self._makeerrorresult(_('malformed argument frame: ' + 'partial argument name')) + + argvalue = payload[offset:] + + # Argument value spans multiple frames. Record our active state + # and wait for the next frame. + if frameflags & FLAG_COMMAND_ARGUMENT_CONTINUATION: + raise error.ProgrammingError('not yet implemented') + self._activeargname = argname + self._activeargchunks = [argvalue] + self._state = 'command-arg-continuation' + return self._makewantframeresult() + + # Common case: the argument value is completely contained in this + # frame. + + if len(argvalue) != valuesize: + self._state = 'errored' + return self._makeerrorresult(_('malformed argument frame: ' + 'partial argument value')) + + self._activeargs[argname] = argvalue + + if frameflags & FLAG_COMMAND_ARGUMENT_EOA: + if self._expectingdata: + self._state = 'command-receiving-data' + # TODO signal request to run a command once we don't + # buffer data frames. + return self._makewantframeresult() + else: + self._state = 'waiting' + return self._makeruncommandresult() + else: + return self._makewantframeresult() + + def _onframereceivingdata(self, frametype, frameflags, payload): + if frametype != FRAME_TYPE_COMMAND_DATA: + self._state = 'errored' + return self._makeerrorresult(_('expected command data frame; ' + 'got %d') % frametype) + + # TODO support streaming data instead of buffering it. + self._activedata.write(payload) + + if frameflags & FLAG_COMMAND_DATA_CONTINUATION: + return self._makewantframeresult() + elif frameflags & FLAG_COMMAND_DATA_EOS: + self._activedata.seek(0) + self._state = 'idle' + return self._makeruncommandresult() + else: + self._state = 'errored' + return self._makeerrorresult(_('command data frame without ' + 'flags')) + + def _onframeerrored(self, frametype, frameflags, payload): + return self._makeerrorresult(_('server already errored')) diff --git a/mercurial/wireprotoserver.py b/mercurial/wireprotoserver.py --- a/mercurial/wireprotoserver.py +++ b/mercurial/wireprotoserver.py @@ -19,6 +19,7 @@ pycompat, util, wireproto, + wireprotoframing, wireprototypes, ) @@ -319,6 +320,11 @@ res.setbodybytes('permission denied') return + # We have a special endpoint to reflect the request back at the client. + if command == b'debugreflect': + _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res) + return + if command not in wireproto.commands: res.status = b'404 Not Found' res.headers[b'Content-Type'] = b'text/plain' @@ -343,8 +349,7 @@ % FRAMINGTYPE) return - if (b'Content-Type' in req.headers - and req.headers[b'Content-Type'] != FRAMINGTYPE): + if req.headers.get(b'Content-Type') != FRAMINGTYPE: res.status = b'415 Unsupported Media Type' # TODO we should send a response with appropriate media type, # since client does Accept it. @@ -358,6 +363,48 @@ res.headers[b'Content-Type'] = b'text/plain' res.setbodybytes(b'/'.join(urlparts) + b'\n') +def _processhttpv2reflectrequest(ui, repo, req, res): + """Reads unified frame protocol request and dumps out state to client. + + This special endpoint can be used to help debug the wire protocol. + + Instead of routing the request through the normal dispatch mechanism, + we instead read all frames, decode them, and feed them into our state + tracker. We then dump the log of all that activity back out to the + client. + """ + import json + + # Reflection APIs have a history of being abused, accidentally disclosing + # sensitive data, etc. So we have a config knob. + if not ui.configbool('experimental', 'web.api.debugreflect'): + res.status = b'404 Not Found' + res.headers[b'Content-Type'] = b'text/plain' + res.setbodybytes(_('debugreflect service not available')) + return + + # We assume we have a unified framing protocol request body. + + reactor = wireprotoframing.serverreactor(ui, repo) + states = [] + + while True: + frame = wireprotoframing.readframe(req.bodyfh) + + if not frame: + states.append(b'received: ') + break + + frametype, frameflags, payload = frame + states.append(b'received: %d %d %s' % (frametype, frameflags, payload)) + + r = reactor.onframerecv(frametype, frameflags, payload) + states.append(json.dumps(r, sort_keys=True, separators=(', ', ': '))) + + res.status = b'200 OK' + res.headers[b'Content-Type'] = b'text/plain' + res.setbodybytes(b'\n'.join(states)) + # Maps API name to metadata so custom API can be registered. API_HANDLERS = { HTTPV2: { diff --git a/tests/test-http-api-httpv2.t b/tests/test-http-api-httpv2.t --- a/tests/test-http-api-httpv2.t +++ b/tests/test-http-api-httpv2.t @@ -261,7 +261,7 @@ > allow-push = * > EOF - $ hg -R server serve -p $HGPORT -d --pid-file hg.pid + $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log $ cat hg.pid > $DAEMON_PIDS Authorized request for valid read-write command works @@ -314,3 +314,441 @@ s> Content-Length: 42\r\n s> \r\n s> unknown wire protocol command: badcommand\n + +debugreflect isn't enabled by default + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > user-agent: test + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> user-agent: test\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> makefile('rb', None) + s> HTTP/1.1 404 Not Found\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 34\r\n + s> \r\n + s> debugreflect service not available + +Restart server to get debugreflect endpoint + + $ killdaemons.py + $ cat > server/.hg/hgrc << EOF + > [experimental] + > web.apiserver = true + > web.api.debugreflect = true + > web.api.http-v2 = true + > [web] + > push_ssl = false + > allow-push = * + > EOF + + $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log + $ cat hg.pid > $DAEMON_PIDS + +Single command frame is decoded + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > accept: $MEDIATYPE + > content-type: $MEDIATYPE + > user-agent: test + > frame command-name eos command1 + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-exp-framing-0001\r\n + s> content-type: application/mercurial-exp-framing-0001\r\n + s> user-agent: test\r\n + s> content-length: 12\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> \x08\x00\x00\x11command1 + s> makefile('rb', None) + s> HTTP/1.1 200 OK\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 117\r\n + s> \r\n + s> received: 1 1 command1\n + s> {"action": "runcommand", "args": {}, "command": "command1", "data": null}\n + s> received: + +Single argument frame is received + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > accept: $MEDIATYPE + > content-type: $MEDIATYPE + > user-agent: test + > frame command-name have-args command1 + > frame command-argument eoa \x03\x00\x05\x00foovalue + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-exp-framing-0001\r\n + s> content-type: application/mercurial-exp-framing-0001\r\n + s> user-agent: test\r\n + s> content-length: 28\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> \x08\x00\x00\x12command1\x0c\x00\x00"\x03\x00\x05\x00foovalue + s> makefile('rb', None) + s> HTTP/1.1 200 OK\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 217\r\n + s> \r\n + s> received: 1 2 command1\n + s> {"action": "wantframe", "state": "command-receiving-args"}\n + s> received: 2 2 \x03\x00\x05\x00foovalue\n + s> {"action": "runcommand", "args": {"foo": "value"}, "command": "command1", "data": null}\n + s> received: + +Multiple argument frames are received + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > accept: $MEDIATYPE + > content-type: $MEDIATYPE + > user-agent: test + > frame command-name have-args command1 + > frame command-argument 0 \x03\x00\x05\x00foovalue + > frame command-argument eoa \x04\x00\x04\x00foo1val1 + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-exp-framing-0001\r\n + s> content-type: application/mercurial-exp-framing-0001\r\n + s> user-agent: test\r\n + s> content-length: 44\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> \x08\x00\x00\x12command1\x0c\x00\x00 \x03\x00\x05\x00foovalue\x0c\x00\x00"\x04\x00\x04\x00foo1val1 + s> makefile('rb', None) + s> HTTP/1.1 200 OK\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 319\r\n + s> \r\n + s> received: 1 2 command1\n + s> {"action": "wantframe", "state": "command-receiving-args"}\n + s> received: 2 0 \x03\x00\x05\x00foovalue\n + s> {"action": "wantframe", "state": "command-receiving-args"}\n + s> received: 2 2 \x04\x00\x04\x00foo1val1\n + s> {"action": "runcommand", "args": {"foo": "value", "foo1": "val1"}, "command": "command1", "data": null}\n + s> received: + +Command with single data frame + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > accept: $MEDIATYPE + > content-type: $MEDIATYPE + > user-agent: test + > frame command-name have-data command1 + > frame command-data eos data! + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-exp-framing-0001\r\n + s> content-type: application/mercurial-exp-framing-0001\r\n + s> user-agent: test\r\n + s> content-length: 21\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> \x08\x00\x00\x14command1\x05\x00\x002data! + s> makefile('rb', None) + s> HTTP/1.1 200 OK\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 199\r\n + s> \r\n + s> received: 1 4 command1\n + s> {"action": "wantframe", "state": "command-receiving-data"}\n + s> received: 3 2 data!\n + s> {"action": "runcommand", "args": {}, "command": "command1", "data": "data!"}\n + s> received: + +Command with multiple data frames + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > accept: $MEDIATYPE + > content-type: $MEDIATYPE + > user-agent: test + > frame command-name have-data command1 + > frame command-data continuation data1 + > frame command-data continuation data2 + > frame command-data eos data3 + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-exp-framing-0001\r\n + s> content-type: application/mercurial-exp-framing-0001\r\n + s> user-agent: test\r\n + s> content-length: 39\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> \x08\x00\x00\x14command1\x05\x00\x001data1\x05\x00\x001data2\x05\x00\x002data3 + s> makefile('rb', None) + s> HTTP/1.1 200 OK\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 367\r\n + s> \r\n + s> received: 1 4 command1\n + s> {"action": "wantframe", "state": "command-receiving-data"}\n + s> received: 3 1 data1\n + s> {"action": "wantframe", "state": "command-receiving-data"}\n + s> received: 3 1 data2\n + s> {"action": "wantframe", "state": "command-receiving-data"}\n + s> received: 3 2 data3\n + s> {"action": "runcommand", "args": {}, "command": "command1", "data": "data1data2data3"}\n + s> received: + +Unexpected frame type + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > accept: $MEDIATYPE + > content-type: $MEDIATYPE + > user-agent: test + > frame command-argument 0 0 + > frame command-name eos mycommand + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-exp-framing-0001\r\n + s> content-type: application/mercurial-exp-framing-0001\r\n + s> user-agent: test\r\n + s> content-length: 18\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> \x01\x00\x00 0 \x00\x00\x11mycommand + s> makefile('rb', None) + s> HTTP/1.1 200 OK\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 181\r\n + s> \r\n + s> received: 2 0 0\n + s> {"action": "error", "message": "expected command frame; got 2"}\n + s> received: 1 1 mycommand\n + s> {"action": "error", "message": "server already errored"}\n + s> received: + +Missing flags on command name frame + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > accept: $MEDIATYPE + > content-type: $MEDIATYPE + > user-agent: test + > frame command-name 0 command1 + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-exp-framing-0001\r\n + s> content-type: application/mercurial-exp-framing-0001\r\n + s> user-agent: test\r\n + s> content-length: 12\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> \x08\x00\x00\x10command1 + s> makefile('rb', None) + s> HTTP/1.1 200 OK\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 114\r\n + s> \r\n + s> received: 1 0 command1\n + s> {"action": "error", "message": "missing frame flags on command frame"}\n + s> received: + +Missing argument frame + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > accept: $MEDIATYPE + > content-type: $MEDIATYPE + > user-agent: test + > frame command-name have-args command1 + > frame command-name 0 ignored + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-exp-framing-0001\r\n + s> content-type: application/mercurial-exp-framing-0001\r\n + s> user-agent: test\r\n + s> content-length: 23\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> \x08\x00\x00\x12command1\x07\x00\x00\x10ignored + s> makefile('rb', None) + s> HTTP/1.1 200 OK\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 197\r\n + s> \r\n + s> received: 1 2 command1\n + s> {"action": "wantframe", "state": "command-receiving-args"}\n + s> received: 1 0 ignored\n + s> {"action": "error", "message": "expected command argument frame; got 1"}\n + s> received: + +Argument frame with incomplete name + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > accept: $MEDIATYPE + > content-type: $MEDIATYPE + > user-agent: test + > frame command-name have-args command1 + > frame command-argument eoa \x04\x00\xde\xadfoo + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-exp-framing-0001\r\n + s> content-type: application/mercurial-exp-framing-0001\r\n + s> user-agent: test\r\n + s> content-length: 23\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> \x08\x00\x00\x12command1\x07\x00\x00"\x04\x00\xde\xadfoo + s> makefile('rb', None) + s> HTTP/1.1 200 OK\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 206\r\n + s> \r\n + s> received: 1 2 command1\n + s> {"action": "wantframe", "state": "command-receiving-args"}\n + s> received: 2 2 \x04\x00\xde\xadfoo\n + s> {"action": "error", "message": "malformed argument frame: partial argument name"}\n + s> received: + +Argument frame with incomplete value + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > accept: $MEDIATYPE + > content-type: $MEDIATYPE + > user-agent: test + > frame command-name have-args command1 + > frame command-argument eoa \x03\x00\xaa\xaafoopartialvalue + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-exp-framing-0001\r\n + s> content-type: application/mercurial-exp-framing-0001\r\n + s> user-agent: test\r\n + s> content-length: 35\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> \x08\x00\x00\x12command1\x13\x00\x00"\x03\x00\xaa\xaafoopartialvalue + s> makefile('rb', None) + s> HTTP/1.1 200 OK\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 219\r\n + s> \r\n + s> received: 1 2 command1\n + s> {"action": "wantframe", "state": "command-receiving-args"}\n + s> received: 2 2 \x03\x00\xaa\xaafoopartialvalue\n + s> {"action": "error", "message": "malformed argument frame: partial argument value"}\n + s> received: + +Missing command data frame + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > accept: $MEDIATYPE + > content-type: $MEDIATYPE + > user-agent: test + > frame command-name have-data command1 + > frame command-name eos command2 + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-exp-framing-0001\r\n + s> content-type: application/mercurial-exp-framing-0001\r\n + s> user-agent: test\r\n + s> content-length: 24\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> \x08\x00\x00\x14command1\x08\x00\x00\x11command2 + s> makefile('rb', None) + s> HTTP/1.1 200 OK\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 194\r\n + s> \r\n + s> received: 1 4 command1\n + s> {"action": "wantframe", "state": "command-receiving-data"}\n + s> received: 1 1 command2\n + s> {"action": "error", "message": "expected command data frame; got 1"}\n + s> received: + +No flags on command data frame + + $ send << EOF + > httprequest POST api/$HTTPV2/ro/debugreflect + > accept: $MEDIATYPE + > content-type: $MEDIATYPE + > user-agent: test + > frame command-name have-data command1 + > frame command-data 0 data + > EOF + using raw connection to peer + s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-exp-framing-0001\r\n + s> content-type: application/mercurial-exp-framing-0001\r\n + s> user-agent: test\r\n + s> content-length: 20\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> \x08\x00\x00\x14command1\x04\x00\x000data + s> makefile('rb', None) + s> HTTP/1.1 200 OK\r\n + s> Server: testing stub value\r\n + s> Date: $HTTP_DATE$\r\n + s> Content-Type: text/plain\r\n + s> Content-Length: 188\r\n + s> \r\n + s> received: 1 4 command1\n + s> {"action": "wantframe", "state": "command-receiving-data"}\n + s> received: 3 0 data\n + s> {"action": "error", "message": "command data frame without flags"}\n + s> received: + + $ cat error.log