mercurial/httppeer.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43089 c59eb1560c44
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    60     # Note: it is *NOT* a bug that the last bit here is a bytestring
    60     # Note: it is *NOT* a bug that the last bit here is a bytestring
    61     # and not a unicode: we're just getting the encoded length anyway,
    61     # and not a unicode: we're just getting the encoded length anyway,
    62     # and using an r-string to make it portable between Python 2 and 3
    62     # and using an r-string to make it portable between Python 2 and 3
    63     # doesn't work because then the \r is a literal backslash-r
    63     # doesn't work because then the \r is a literal backslash-r
    64     # instead of a carriage return.
    64     # instead of a carriage return.
    65     valuelen = limit - len(fmt % r'000') - len(': \r\n')
    65     valuelen = limit - len(fmt % r'000') - len(b': \r\n')
    66     result = []
    66     result = []
    67 
    67 
    68     n = 0
    68     n = 0
    69     for i in pycompat.xrange(0, len(value), valuelen):
    69     for i in pycompat.xrange(0, len(value), valuelen):
    70         n += 1
    70         n += 1
    74 
    74 
    75 
    75 
    76 class _multifile(object):
    76 class _multifile(object):
    77     def __init__(self, *fileobjs):
    77     def __init__(self, *fileobjs):
    78         for f in fileobjs:
    78         for f in fileobjs:
    79             if not util.safehasattr(f, 'length'):
    79             if not util.safehasattr(f, b'length'):
    80                 raise ValueError(
    80                 raise ValueError(
    81                     '_multifile only supports file objects that '
    81                     b'_multifile only supports file objects that '
    82                     'have a length but this one does not:',
    82                     b'have a length but this one does not:',
    83                     type(f),
    83                     type(f),
    84                     f,
    84                     f,
    85                 )
    85                 )
    86         self._fileobjs = fileobjs
    86         self._fileobjs = fileobjs
    87         self._index = 0
    87         self._index = 0
    90     def length(self):
    90     def length(self):
    91         return sum(f.length for f in self._fileobjs)
    91         return sum(f.length for f in self._fileobjs)
    92 
    92 
    93     def read(self, amt=None):
    93     def read(self, amt=None):
    94         if amt <= 0:
    94         if amt <= 0:
    95             return ''.join(f.read() for f in self._fileobjs)
    95             return b''.join(f.read() for f in self._fileobjs)
    96         parts = []
    96         parts = []
    97         while amt and self._index < len(self._fileobjs):
    97         while amt and self._index < len(self._fileobjs):
    98             parts.append(self._fileobjs[self._index].read(amt))
    98             parts.append(self._fileobjs[self._index].read(amt))
    99             got = len(parts[-1])
    99             got = len(parts[-1])
   100             if got < amt:
   100             if got < amt:
   101                 self._index += 1
   101                 self._index += 1
   102             amt -= got
   102             amt -= got
   103         return ''.join(parts)
   103         return b''.join(parts)
   104 
   104 
   105     def seek(self, offset, whence=os.SEEK_SET):
   105     def seek(self, offset, whence=os.SEEK_SET):
   106         if whence != os.SEEK_SET:
   106         if whence != os.SEEK_SET:
   107             raise NotImplementedError(
   107             raise NotImplementedError(
   108                 '_multifile does not support anything other'
   108                 b'_multifile does not support anything other'
   109                 ' than os.SEEK_SET for whence on seek()'
   109                 b' than os.SEEK_SET for whence on seek()'
   110             )
   110             )
   111         if offset != 0:
   111         if offset != 0:
   112             raise NotImplementedError(
   112             raise NotImplementedError(
   113                 '_multifile only supports seeking to start, but that '
   113                 b'_multifile only supports seeking to start, but that '
   114                 'could be fixed if you need it'
   114                 b'could be fixed if you need it'
   115             )
   115             )
   116         for f in self._fileobjs:
   116         for f in self._fileobjs:
   117             f.seek(0)
   117             f.seek(0)
   118         self._index = 0
   118         self._index = 0
   119 
   119 
   129     ``capablefn`` is a function to evaluate a capability.
   129     ``capablefn`` is a function to evaluate a capability.
   130 
   130 
   131     ``cmd``, ``args``, and ``data`` define the command, its arguments, and
   131     ``cmd``, ``args``, and ``data`` define the command, its arguments, and
   132     raw data to pass to it.
   132     raw data to pass to it.
   133     """
   133     """
   134     if cmd == 'pushkey':
   134     if cmd == b'pushkey':
   135         args['data'] = ''
   135         args[b'data'] = b''
   136     data = args.pop('data', None)
   136     data = args.pop(b'data', None)
   137     headers = args.pop('headers', {})
   137     headers = args.pop(b'headers', {})
   138 
   138 
   139     ui.debug("sending %s command\n" % cmd)
   139     ui.debug(b"sending %s command\n" % cmd)
   140     q = [('cmd', cmd)]
   140     q = [(b'cmd', cmd)]
   141     headersize = 0
   141     headersize = 0
   142     # Important: don't use self.capable() here or else you end up
   142     # Important: don't use self.capable() here or else you end up
   143     # with infinite recursion when trying to look up capabilities
   143     # with infinite recursion when trying to look up capabilities
   144     # for the first time.
   144     # for the first time.
   145     postargsok = caps is not None and 'httppostargs' in caps
   145     postargsok = caps is not None and b'httppostargs' in caps
   146 
   146 
   147     # Send arguments via POST.
   147     # Send arguments via POST.
   148     if postargsok and args:
   148     if postargsok and args:
   149         strargs = urlreq.urlencode(sorted(args.items()))
   149         strargs = urlreq.urlencode(sorted(args.items()))
   150         if not data:
   150         if not data:
   160         headers[r'X-HgArgs-Post'] = len(strargs)
   160         headers[r'X-HgArgs-Post'] = len(strargs)
   161     elif args:
   161     elif args:
   162         # Calling self.capable() can infinite loop if we are calling
   162         # Calling self.capable() can infinite loop if we are calling
   163         # "capabilities". But that command should never accept wire
   163         # "capabilities". But that command should never accept wire
   164         # protocol arguments. So this should never happen.
   164         # protocol arguments. So this should never happen.
   165         assert cmd != 'capabilities'
   165         assert cmd != b'capabilities'
   166         httpheader = capablefn('httpheader')
   166         httpheader = capablefn(b'httpheader')
   167         if httpheader:
   167         if httpheader:
   168             headersize = int(httpheader.split(',', 1)[0])
   168             headersize = int(httpheader.split(b',', 1)[0])
   169 
   169 
   170         # Send arguments via HTTP headers.
   170         # Send arguments via HTTP headers.
   171         if headersize > 0:
   171         if headersize > 0:
   172             # The headers can typically carry more data than the URL.
   172             # The headers can typically carry more data than the URL.
   173             encargs = urlreq.urlencode(sorted(args.items()))
   173             encargs = urlreq.urlencode(sorted(args.items()))
   174             for header, value in encodevalueinheaders(
   174             for header, value in encodevalueinheaders(
   175                 encargs, 'X-HgArg', headersize
   175                 encargs, b'X-HgArg', headersize
   176             ):
   176             ):
   177                 headers[header] = value
   177                 headers[header] = value
   178         # Send arguments via query string (Mercurial <1.9).
   178         # Send arguments via query string (Mercurial <1.9).
   179         else:
   179         else:
   180             q += sorted(args.items())
   180             q += sorted(args.items())
   181 
   181 
   182     qs = '?%s' % urlreq.urlencode(q)
   182     qs = b'?%s' % urlreq.urlencode(q)
   183     cu = "%s%s" % (repobaseurl, qs)
   183     cu = b"%s%s" % (repobaseurl, qs)
   184     size = 0
   184     size = 0
   185     if util.safehasattr(data, 'length'):
   185     if util.safehasattr(data, b'length'):
   186         size = data.length
   186         size = data.length
   187     elif data is not None:
   187     elif data is not None:
   188         size = len(data)
   188         size = len(data)
   189     if data is not None and r'Content-Type' not in headers:
   189     if data is not None and r'Content-Type' not in headers:
   190         headers[r'Content-Type'] = r'application/mercurial-0.1'
   190         headers[r'Content-Type'] = r'application/mercurial-0.1'
   196     # protocol parameters should only occur after the handshake.
   196     # protocol parameters should only occur after the handshake.
   197     protoparams = set()
   197     protoparams = set()
   198 
   198 
   199     mediatypes = set()
   199     mediatypes = set()
   200     if caps is not None:
   200     if caps is not None:
   201         mt = capablefn('httpmediatype')
   201         mt = capablefn(b'httpmediatype')
   202         if mt:
   202         if mt:
   203             protoparams.add('0.1')
   203             protoparams.add(b'0.1')
   204             mediatypes = set(mt.split(','))
   204             mediatypes = set(mt.split(b','))
   205 
   205 
   206         protoparams.add('partial-pull')
   206         protoparams.add(b'partial-pull')
   207 
   207 
   208     if '0.2tx' in mediatypes:
   208     if b'0.2tx' in mediatypes:
   209         protoparams.add('0.2')
   209         protoparams.add(b'0.2')
   210 
   210 
   211     if '0.2tx' in mediatypes and capablefn('compression'):
   211     if b'0.2tx' in mediatypes and capablefn(b'compression'):
   212         # We /could/ compare supported compression formats and prune
   212         # We /could/ compare supported compression formats and prune
   213         # non-mutually supported or error if nothing is mutually supported.
   213         # non-mutually supported or error if nothing is mutually supported.
   214         # For now, send the full list to the server and have it error.
   214         # For now, send the full list to the server and have it error.
   215         comps = [
   215         comps = [
   216             e.wireprotosupport().name
   216             e.wireprotosupport().name
   217             for e in util.compengines.supportedwireengines(util.CLIENTROLE)
   217             for e in util.compengines.supportedwireengines(util.CLIENTROLE)
   218         ]
   218         ]
   219         protoparams.add('comp=%s' % ','.join(comps))
   219         protoparams.add(b'comp=%s' % b','.join(comps))
   220 
   220 
   221     if protoparams:
   221     if protoparams:
   222         protoheaders = encodevalueinheaders(
   222         protoheaders = encodevalueinheaders(
   223             ' '.join(sorted(protoparams)), 'X-HgProto', headersize or 1024
   223             b' '.join(sorted(protoparams)), b'X-HgProto', headersize or 1024
   224         )
   224         )
   225         for header, value in protoheaders:
   225         for header, value in protoheaders:
   226             headers[header] = value
   226             headers[header] = value
   227 
   227 
   228     varyheaders = []
   228     varyheaders = []
   234         headers[r'Vary'] = r','.join(sorted(varyheaders))
   234         headers[r'Vary'] = r','.join(sorted(varyheaders))
   235 
   235 
   236     req = requestbuilder(pycompat.strurl(cu), data, headers)
   236     req = requestbuilder(pycompat.strurl(cu), data, headers)
   237 
   237 
   238     if data is not None:
   238     if data is not None:
   239         ui.debug("sending %d bytes\n" % size)
   239         ui.debug(b"sending %d bytes\n" % size)
   240         req.add_unredirected_header(r'Content-Length', r'%d' % size)
   240         req.add_unredirected_header(r'Content-Length', r'%d' % size)
   241 
   241 
   242     return req, cu, qs
   242     return req, cu, qs
   243 
   243 
   244 
   244 
   255     """Send a prepared HTTP request.
   255     """Send a prepared HTTP request.
   256 
   256 
   257     Returns the response object.
   257     Returns the response object.
   258     """
   258     """
   259     dbg = ui.debug
   259     dbg = ui.debug
   260     if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
   260     if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
   261         line = 'devel-peer-request: %s\n'
   261         line = b'devel-peer-request: %s\n'
   262         dbg(
   262         dbg(
   263             line
   263             line
   264             % '%s %s'
   264             % b'%s %s'
   265             % (
   265             % (
   266                 pycompat.bytesurl(req.get_method()),
   266                 pycompat.bytesurl(req.get_method()),
   267                 pycompat.bytesurl(req.get_full_url()),
   267                 pycompat.bytesurl(req.get_full_url()),
   268             )
   268             )
   269         )
   269         )
   270         hgargssize = None
   270         hgargssize = None
   271 
   271 
   272         for header, value in sorted(req.header_items()):
   272         for header, value in sorted(req.header_items()):
   273             header = pycompat.bytesurl(header)
   273             header = pycompat.bytesurl(header)
   274             value = pycompat.bytesurl(value)
   274             value = pycompat.bytesurl(value)
   275             if header.startswith('X-hgarg-'):
   275             if header.startswith(b'X-hgarg-'):
   276                 if hgargssize is None:
   276                 if hgargssize is None:
   277                     hgargssize = 0
   277                     hgargssize = 0
   278                 hgargssize += len(value)
   278                 hgargssize += len(value)
   279             else:
   279             else:
   280                 dbg(line % '  %s %s' % (header, value))
   280                 dbg(line % b'  %s %s' % (header, value))
   281 
   281 
   282         if hgargssize is not None:
   282         if hgargssize is not None:
   283             dbg(
   283             dbg(
   284                 line
   284                 line
   285                 % '  %d bytes of commands arguments in headers'
   285                 % b'  %d bytes of commands arguments in headers'
   286                 % hgargssize
   286                 % hgargssize
   287             )
   287             )
   288         data = _reqdata(req)
   288         data = _reqdata(req)
   289         if data is not None:
   289         if data is not None:
   290             length = getattr(data, 'length', None)
   290             length = getattr(data, 'length', None)
   291             if length is None:
   291             if length is None:
   292                 length = len(data)
   292                 length = len(data)
   293             dbg(line % '  %d bytes of data' % length)
   293             dbg(line % b'  %d bytes of data' % length)
   294 
   294 
   295         start = util.timer()
   295         start = util.timer()
   296 
   296 
   297     res = None
   297     res = None
   298     try:
   298     try:
   299         res = opener.open(req)
   299         res = opener.open(req)
   300     except urlerr.httperror as inst:
   300     except urlerr.httperror as inst:
   301         if inst.code == 401:
   301         if inst.code == 401:
   302             raise error.Abort(_('authorization failed'))
   302             raise error.Abort(_(b'authorization failed'))
   303         raise
   303         raise
   304     except httplib.HTTPException as inst:
   304     except httplib.HTTPException as inst:
   305         ui.debug(
   305         ui.debug(
   306             'http error requesting %s\n' % util.hidepassword(req.get_full_url())
   306             b'http error requesting %s\n'
       
   307             % util.hidepassword(req.get_full_url())
   307         )
   308         )
   308         ui.traceback()
   309         ui.traceback()
   309         raise IOError(None, inst)
   310         raise IOError(None, inst)
   310     finally:
   311     finally:
   311         if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
   312         if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
   312             code = res.code if res else -1
   313             code = res.code if res else -1
   313             dbg(
   314             dbg(
   314                 line
   315                 line
   315                 % '  finished in %.4f seconds (%d)'
   316                 % b'  finished in %.4f seconds (%d)'
   316                 % (util.timer() - start, code)
   317                 % (util.timer() - start, code)
   317             )
   318             )
   318 
   319 
   319     # Insert error handlers for common I/O failures.
   320     # Insert error handlers for common I/O failures.
   320     urlmod.wrapresponse(res)
   321     urlmod.wrapresponse(res)
   338         respurl = respurl[: -len(qs)]
   339         respurl = respurl[: -len(qs)]
   339         qsdropped = False
   340         qsdropped = False
   340     else:
   341     else:
   341         qsdropped = True
   342         qsdropped = True
   342 
   343 
   343     if baseurl.rstrip('/') != respurl.rstrip('/'):
   344     if baseurl.rstrip(b'/') != respurl.rstrip(b'/'):
   344         redirected = True
   345         redirected = True
   345         if not ui.quiet:
   346         if not ui.quiet:
   346             ui.warn(_('real URL is %s\n') % respurl)
   347             ui.warn(_(b'real URL is %s\n') % respurl)
   347 
   348 
   348     try:
   349     try:
   349         proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
   350         proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
   350     except AttributeError:
   351     except AttributeError:
   351         proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
   352         proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
   352 
   353 
   353     safeurl = util.hidepassword(baseurl)
   354     safeurl = util.hidepassword(baseurl)
   354     if proto.startswith('application/hg-error'):
   355     if proto.startswith(b'application/hg-error'):
   355         raise error.OutOfBandError(resp.read())
   356         raise error.OutOfBandError(resp.read())
   356 
   357 
   357     # Pre 1.0 versions of Mercurial used text/plain and
   358     # Pre 1.0 versions of Mercurial used text/plain and
   358     # application/hg-changegroup. We don't support such old servers.
   359     # application/hg-changegroup. We don't support such old servers.
   359     if not proto.startswith('application/mercurial-'):
   360     if not proto.startswith(b'application/mercurial-'):
   360         ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
   361         ui.debug(b"requested URL: '%s'\n" % util.hidepassword(requrl))
   361         msg = _(
   362         msg = _(
   362             "'%s' does not appear to be an hg repository:\n"
   363             b"'%s' does not appear to be an hg repository:\n"
   363             "---%%<--- (%s)\n%s\n---%%<---\n"
   364             b"---%%<--- (%s)\n%s\n---%%<---\n"
   364         ) % (safeurl, proto or 'no content-type', resp.read(1024))
   365         ) % (safeurl, proto or b'no content-type', resp.read(1024))
   365 
   366 
   366         # Some servers may strip the query string from the redirect. We
   367         # Some servers may strip the query string from the redirect. We
   367         # raise a special error type so callers can react to this specially.
   368         # raise a special error type so callers can react to this specially.
   368         if redirected and qsdropped:
   369         if redirected and qsdropped:
   369             raise RedirectedRepoError(msg, respurl)
   370             raise RedirectedRepoError(msg, respurl)
   370         else:
   371         else:
   371             raise error.RepoError(msg)
   372             raise error.RepoError(msg)
   372 
   373 
   373     try:
   374     try:
   374         subtype = proto.split('-', 1)[1]
   375         subtype = proto.split(b'-', 1)[1]
   375 
   376 
   376         # Unless we end up supporting CBOR in the legacy wire protocol,
   377         # Unless we end up supporting CBOR in the legacy wire protocol,
   377         # this should ONLY be encountered for the initial capabilities
   378         # this should ONLY be encountered for the initial capabilities
   378         # request during handshake.
   379         # request during handshake.
   379         if subtype == 'cbor':
   380         if subtype == b'cbor':
   380             if allowcbor:
   381             if allowcbor:
   381                 return respurl, proto, resp
   382                 return respurl, proto, resp
   382             else:
   383             else:
   383                 raise error.RepoError(
   384                 raise error.RepoError(
   384                     _('unexpected CBOR response from ' 'server')
   385                     _(b'unexpected CBOR response from ' b'server')
   385                 )
   386                 )
   386 
   387 
   387         version_info = tuple([int(n) for n in subtype.split('.')])
   388         version_info = tuple([int(n) for n in subtype.split(b'.')])
   388     except ValueError:
   389     except ValueError:
   389         raise error.RepoError(
   390         raise error.RepoError(
   390             _("'%s' sent a broken Content-Type " "header (%s)")
   391             _(b"'%s' sent a broken Content-Type " b"header (%s)")
   391             % (safeurl, proto)
   392             % (safeurl, proto)
   392         )
   393         )
   393 
   394 
   394     # TODO consider switching to a decompression reader that uses
   395     # TODO consider switching to a decompression reader that uses
   395     # generators.
   396     # generators.
   396     if version_info == (0, 1):
   397     if version_info == (0, 1):
   397         if compressible:
   398         if compressible:
   398             resp = util.compengines['zlib'].decompressorreader(resp)
   399             resp = util.compengines[b'zlib'].decompressorreader(resp)
   399 
   400 
   400     elif version_info == (0, 2):
   401     elif version_info == (0, 2):
   401         # application/mercurial-0.2 always identifies the compression
   402         # application/mercurial-0.2 always identifies the compression
   402         # engine in the payload header.
   403         # engine in the payload header.
   403         elen = struct.unpack('B', util.readexactly(resp, 1))[0]
   404         elen = struct.unpack(b'B', util.readexactly(resp, 1))[0]
   404         ename = util.readexactly(resp, elen)
   405         ename = util.readexactly(resp, elen)
   405         engine = util.compengines.forwiretype(ename)
   406         engine = util.compengines.forwiretype(ename)
   406 
   407 
   407         resp = engine.decompressorreader(resp)
   408         resp = engine.decompressorreader(resp)
   408     else:
   409     else:
   409         raise error.RepoError(
   410         raise error.RepoError(
   410             _("'%s' uses newer protocol %s") % (safeurl, subtype)
   411             _(b"'%s' uses newer protocol %s") % (safeurl, subtype)
   411         )
   412         )
   412 
   413 
   413     return respurl, proto, resp
   414     return respurl, proto, resp
   414 
   415 
   415 
   416 
   417     def __init__(self, ui, path, url, opener, requestbuilder, caps):
   418     def __init__(self, ui, path, url, opener, requestbuilder, caps):
   418         self.ui = ui
   419         self.ui = ui
   419         self._path = path
   420         self._path = path
   420         self._url = url
   421         self._url = url
   421         self._caps = caps
   422         self._caps = caps
   422         self.limitedarguments = caps is not None and 'httppostargs' not in caps
   423         self.limitedarguments = caps is not None and b'httppostargs' not in caps
   423         self._urlopener = opener
   424         self._urlopener = opener
   424         self._requestbuilder = requestbuilder
   425         self._requestbuilder = requestbuilder
   425 
   426 
   426     def __del__(self):
   427     def __del__(self):
   427         for h in self._urlopener.handlers:
   428         for h in self._urlopener.handlers:
   451             )
   452             )
   452         except AttributeError:
   453         except AttributeError:
   453             return
   454             return
   454         self.ui.note(
   455         self.ui.note(
   455             _(
   456             _(
   456                 '(sent %d HTTP requests and %d bytes; '
   457                 b'(sent %d HTTP requests and %d bytes; '
   457                 'received %d bytes in responses)\n'
   458                 b'received %d bytes in responses)\n'
   458             )
   459             )
   459             % (reqs, sent, recv)
   460             % (reqs, sent, recv)
   460         )
   461         )
   461 
   462 
   462     # End of ipeerconnection interface.
   463     # End of ipeerconnection interface.
   499 
   500 
   500     def _callpush(self, cmd, cg, **args):
   501     def _callpush(self, cmd, cg, **args):
   501         # have to stream bundle to a temp file because we do not have
   502         # have to stream bundle to a temp file because we do not have
   502         # http 1.1 chunked transfer.
   503         # http 1.1 chunked transfer.
   503 
   504 
   504         types = self.capable('unbundle')
   505         types = self.capable(b'unbundle')
   505         try:
   506         try:
   506             types = types.split(',')
   507             types = types.split(b',')
   507         except AttributeError:
   508         except AttributeError:
   508             # servers older than d1b16a746db6 will send 'unbundle' as a
   509             # servers older than d1b16a746db6 will send 'unbundle' as a
   509             # boolean capability. They only support headerless/uncompressed
   510             # boolean capability. They only support headerless/uncompressed
   510             # bundles.
   511             # bundles.
   511             types = [""]
   512             types = [b""]
   512         for x in types:
   513         for x in types:
   513             if x in bundle2.bundletypes:
   514             if x in bundle2.bundletypes:
   514                 type = x
   515                 type = x
   515                 break
   516                 break
   516 
   517 
   517         tempname = bundle2.writebundle(self.ui, cg, None, type)
   518         tempname = bundle2.writebundle(self.ui, cg, None, type)
   518         fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
   519         fp = httpconnection.httpsendfile(self.ui, tempname, b"rb")
   519         headers = {r'Content-Type': r'application/mercurial-0.1'}
   520         headers = {r'Content-Type': r'application/mercurial-0.1'}
   520 
   521 
   521         try:
   522         try:
   522             r = self._call(cmd, data=fp, headers=headers, **args)
   523             r = self._call(cmd, data=fp, headers=headers, **args)
   523             vals = r.split('\n', 1)
   524             vals = r.split(b'\n', 1)
   524             if len(vals) < 2:
   525             if len(vals) < 2:
   525                 raise error.ResponseError(_("unexpected response:"), r)
   526                 raise error.ResponseError(_(b"unexpected response:"), r)
   526             return vals
   527             return vals
   527         except urlerr.httperror:
   528         except urlerr.httperror:
   528             # Catch and re-raise these so we don't try and treat them
   529             # Catch and re-raise these so we don't try and treat them
   529             # like generic socket errors. They lack any values in
   530             # like generic socket errors. They lack any values in
   530             # .args on Python 3 which breaks our socket.error block.
   531             # .args on Python 3 which breaks our socket.error block.
   531             raise
   532             raise
   532         except socket.error as err:
   533         except socket.error as err:
   533             if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
   534             if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
   534                 raise error.Abort(_('push failed: %s') % err.args[1])
   535                 raise error.Abort(_(b'push failed: %s') % err.args[1])
   535             raise error.Abort(err.args[1])
   536             raise error.Abort(err.args[1])
   536         finally:
   537         finally:
   537             fp.close()
   538             fp.close()
   538             os.unlink(tempname)
   539             os.unlink(tempname)
   539 
   540 
   540     def _calltwowaystream(self, cmd, fp, **args):
   541     def _calltwowaystream(self, cmd, fp, **args):
   541         filename = None
   542         filename = None
   542         try:
   543         try:
   543             # dump bundle to disk
   544             # dump bundle to disk
   544             fd, filename = pycompat.mkstemp(prefix="hg-bundle-", suffix=".hg")
   545             fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
   545             with os.fdopen(fd, r"wb") as fh:
   546             with os.fdopen(fd, r"wb") as fh:
   546                 d = fp.read(4096)
   547                 d = fp.read(4096)
   547                 while d:
   548                 while d:
   548                     fh.write(d)
   549                     fh.write(d)
   549                     d = fp.read(4096)
   550                     d = fp.read(4096)
   550             # start http push
   551             # start http push
   551             with httpconnection.httpsendfile(self.ui, filename, "rb") as fp_:
   552             with httpconnection.httpsendfile(self.ui, filename, b"rb") as fp_:
   552                 headers = {r'Content-Type': r'application/mercurial-0.1'}
   553                 headers = {r'Content-Type': r'application/mercurial-0.1'}
   553                 return self._callstream(cmd, data=fp_, headers=headers, **args)
   554                 return self._callstream(cmd, data=fp_, headers=headers, **args)
   554         finally:
   555         finally:
   555             if filename is not None:
   556             if filename is not None:
   556                 os.unlink(filename)
   557                 os.unlink(filename)
   596 
   597 
   597     handler = wireprotov2peer.clienthandler(
   598     handler = wireprotov2peer.clienthandler(
   598         ui, reactor, opener=opener, requestbuilder=requestbuilder
   599         ui, reactor, opener=opener, requestbuilder=requestbuilder
   599     )
   600     )
   600 
   601 
   601     url = '%s/%s' % (apiurl, permission)
   602     url = b'%s/%s' % (apiurl, permission)
   602 
   603 
   603     if len(requests) > 1:
   604     if len(requests) > 1:
   604         url += '/multirequest'
   605         url += b'/multirequest'
   605     else:
   606     else:
   606         url += '/%s' % requests[0][0]
   607         url += b'/%s' % requests[0][0]
   607 
   608 
   608     ui.debug('sending %d commands\n' % len(requests))
   609     ui.debug(b'sending %d commands\n' % len(requests))
   609     for command, args, f in requests:
   610     for command, args, f in requests:
   610         ui.debug(
   611         ui.debug(
   611             'sending command %s: %s\n'
   612             b'sending command %s: %s\n'
   612             % (command, stringutil.pprint(args, indent=2))
   613             % (command, stringutil.pprint(args, indent=2))
   613         )
   614         )
   614         assert not list(
   615         assert not list(
   615             handler.callcommand(command, args, f, redirect=redirect)
   616             handler.callcommand(command, args, f, redirect=redirect)
   616         )
   617         )
   629 
   630 
   630     try:
   631     try:
   631         res = opener.open(req)
   632         res = opener.open(req)
   632     except urlerr.httperror as e:
   633     except urlerr.httperror as e:
   633         if e.code == 401:
   634         if e.code == 401:
   634             raise error.Abort(_('authorization failed'))
   635             raise error.Abort(_(b'authorization failed'))
   635 
   636 
   636         raise
   637         raise
   637     except httplib.HTTPException as e:
   638     except httplib.HTTPException as e:
   638         ui.traceback()
   639         ui.traceback()
   639         raise IOError(None, e)
   640         raise IOError(None, e)
   681         self.close()
   682         self.close()
   682 
   683 
   683     def callcommand(self, command, args):
   684     def callcommand(self, command, args):
   684         if self._sent:
   685         if self._sent:
   685             raise error.ProgrammingError(
   686             raise error.ProgrammingError(
   686                 'callcommand() cannot be used after ' 'commands are sent'
   687                 b'callcommand() cannot be used after ' b'commands are sent'
   687             )
   688             )
   688 
   689 
   689         if self._closed:
   690         if self._closed:
   690             raise error.ProgrammingError(
   691             raise error.ProgrammingError(
   691                 'callcommand() cannot be used after ' 'close()'
   692                 b'callcommand() cannot be used after ' b'close()'
   692             )
   693             )
   693 
   694 
   694         # The service advertises which commands are available. So if we attempt
   695         # The service advertises which commands are available. So if we attempt
   695         # to call an unknown command or pass an unknown argument, we can screen
   696         # to call an unknown command or pass an unknown argument, we can screen
   696         # for this.
   697         # for this.
   697         if command not in self._descriptor['commands']:
   698         if command not in self._descriptor[b'commands']:
   698             raise error.ProgrammingError(
   699             raise error.ProgrammingError(
   699                 'wire protocol command %s is not available' % command
   700                 b'wire protocol command %s is not available' % command
   700             )
   701             )
   701 
   702 
   702         cmdinfo = self._descriptor['commands'][command]
   703         cmdinfo = self._descriptor[b'commands'][command]
   703         unknownargs = set(args.keys()) - set(cmdinfo.get('args', {}))
   704         unknownargs = set(args.keys()) - set(cmdinfo.get(b'args', {}))
   704 
   705 
   705         if unknownargs:
   706         if unknownargs:
   706             raise error.ProgrammingError(
   707             raise error.ProgrammingError(
   707                 'wire protocol command %s does not accept argument: %s'
   708                 b'wire protocol command %s does not accept argument: %s'
   708                 % (command, ', '.join(sorted(unknownargs)))
   709                 % (command, b', '.join(sorted(unknownargs)))
   709             )
   710             )
   710 
   711 
   711         self._neededpermissions |= set(cmdinfo['permissions'])
   712         self._neededpermissions |= set(cmdinfo[b'permissions'])
   712 
   713 
   713         # TODO we /could/ also validate types here, since the API descriptor
   714         # TODO we /could/ also validate types here, since the API descriptor
   714         # includes types...
   715         # includes types...
   715 
   716 
   716         f = pycompat.futures.Future()
   717         f = pycompat.futures.Future()
   754         if not calls:
   755         if not calls:
   755             return
   756             return
   756 
   757 
   757         permissions = set(self._neededpermissions)
   758         permissions = set(self._neededpermissions)
   758 
   759 
   759         if 'push' in permissions and 'pull' in permissions:
   760         if b'push' in permissions and b'pull' in permissions:
   760             permissions.remove('pull')
   761             permissions.remove(b'pull')
   761 
   762 
   762         if len(permissions) > 1:
   763         if len(permissions) > 1:
   763             raise error.RepoError(
   764             raise error.RepoError(
   764                 _('cannot make request requiring multiple ' 'permissions: %s')
   765                 _(b'cannot make request requiring multiple ' b'permissions: %s')
   765                 % _(', ').join(sorted(permissions))
   766                 % _(b', ').join(sorted(permissions))
   766             )
   767             )
   767 
   768 
   768         permission = {'push': 'rw', 'pull': 'ro',}[permissions.pop()]
   769         permission = {b'push': b'rw', b'pull': b'ro',}[permissions.pop()]
   769 
   770 
   770         handler, resp = sendv2request(
   771         handler, resp = sendv2request(
   771             self._ui,
   772             self._ui,
   772             self._opener,
   773             self._opener,
   773             self._requestbuilder,
   774             self._requestbuilder,
   807             # If any of our futures are still in progress, mark them as
   808             # If any of our futures are still in progress, mark them as
   808             # errored, otherwise a result() could wait indefinitely.
   809             # errored, otherwise a result() could wait indefinitely.
   809             for f in self._futures:
   810             for f in self._futures:
   810                 if not f.done():
   811                 if not f.done():
   811                     f.set_exception(
   812                     f.set_exception(
   812                         error.ResponseError(_('unfulfilled command response'))
   813                         error.ResponseError(_(b'unfulfilled command response'))
   813                     )
   814                     )
   814 
   815 
   815             self._futures = None
   816             self._futures = None
   816 
   817 
   817     def _handleresponse(self, handler, resp):
   818     def _handleresponse(self, handler, resp):
   830         self, ui, repourl, apipath, opener, requestbuilder, apidescriptor
   831         self, ui, repourl, apipath, opener, requestbuilder, apidescriptor
   831     ):
   832     ):
   832         self.ui = ui
   833         self.ui = ui
   833         self.apidescriptor = apidescriptor
   834         self.apidescriptor = apidescriptor
   834 
   835 
   835         if repourl.endswith('/'):
   836         if repourl.endswith(b'/'):
   836             repourl = repourl[:-1]
   837             repourl = repourl[:-1]
   837 
   838 
   838         self._url = repourl
   839         self._url = repourl
   839         self._apipath = apipath
   840         self._apipath = apipath
   840         self._apiurl = '%s/%s' % (repourl, apipath)
   841         self._apiurl = b'%s/%s' % (repourl, apipath)
   841         self._opener = opener
   842         self._opener = opener
   842         self._requestbuilder = requestbuilder
   843         self._requestbuilder = requestbuilder
   843 
   844 
   844         self._redirect = wireprotov2peer.supportedredirects(ui, apidescriptor)
   845         self._redirect = wireprotov2peer.supportedredirects(ui, apidescriptor)
   845 
   846 
   859         return False
   860         return False
   860 
   861 
   861     def close(self):
   862     def close(self):
   862         self.ui.note(
   863         self.ui.note(
   863             _(
   864             _(
   864                 '(sent %d HTTP requests and %d bytes; '
   865                 b'(sent %d HTTP requests and %d bytes; '
   865                 'received %d bytes in responses)\n'
   866                 b'received %d bytes in responses)\n'
   866             )
   867             )
   867             % (
   868             % (
   868                 self._opener.requestscount,
   869                 self._opener.requestscount,
   869                 self._opener.sentbytescount,
   870                 self._opener.sentbytescount,
   870                 self._opener.receivedbytescount,
   871                 self._opener.receivedbytescount,
   879         # The capabilities used internally historically map to capabilities
   880         # The capabilities used internally historically map to capabilities
   880         # advertised from the "capabilities" wire protocol command. However,
   881         # advertised from the "capabilities" wire protocol command. However,
   881         # version 2 of that command works differently.
   882         # version 2 of that command works differently.
   882 
   883 
   883         # Maps to commands that are available.
   884         # Maps to commands that are available.
   884         if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
   885         if name in (
       
   886             b'branchmap',
       
   887             b'getbundle',
       
   888             b'known',
       
   889             b'lookup',
       
   890             b'pushkey',
       
   891         ):
   885             return True
   892             return True
   886 
   893 
   887         # Other concepts.
   894         # Other concepts.
   888         if name in 'bundle2':
   895         if name in b'bundle2':
   889             return True
   896             return True
   890 
   897 
   891         # Alias command-* to presence of command of that name.
   898         # Alias command-* to presence of command of that name.
   892         if name.startswith('command-'):
   899         if name.startswith(b'command-'):
   893             return name[len('command-') :] in self.apidescriptor['commands']
   900             return name[len(b'command-') :] in self.apidescriptor[b'commands']
   894 
   901 
   895         return False
   902         return False
   896 
   903 
   897     def requirecap(self, name, purpose):
   904     def requirecap(self, name, purpose):
   898         if self.capable(name):
   905         if self.capable(name):
   899             return
   906             return
   900 
   907 
   901         raise error.CapabilityError(
   908         raise error.CapabilityError(
   902             _(
   909             _(
   903                 'cannot %s; client or remote repository does not support the '
   910                 b'cannot %s; client or remote repository does not support the '
   904                 '\'%s\' capability'
   911                 b'\'%s\' capability'
   905             )
   912             )
   906             % (purpose, name)
   913             % (purpose, name)
   907         )
   914         )
   908 
   915 
   909     # End of ipeercapabilities.
   916     # End of ipeercapabilities.
   933 #
   940 #
   934 # priority
   941 # priority
   935 #    Integer priority for the service. If we could choose from multiple
   942 #    Integer priority for the service. If we could choose from multiple
   936 #    services, we choose the one with the highest priority.
   943 #    services, we choose the one with the highest priority.
   937 API_PEERS = {
   944 API_PEERS = {
   938     wireprototypes.HTTP_WIREPROTO_V2: {'init': httpv2peer, 'priority': 50,},
   945     wireprototypes.HTTP_WIREPROTO_V2: {b'init': httpv2peer, b'priority': 50,},
   939 }
   946 }
   940 
   947 
   941 
   948 
   942 def performhandshake(ui, url, opener, requestbuilder):
   949 def performhandshake(ui, url, opener, requestbuilder):
   943     # The handshake is a request to the capabilities command.
   950     # The handshake is a request to the capabilities command.
   944 
   951 
   945     caps = None
   952     caps = None
   946 
   953 
   947     def capable(x):
   954     def capable(x):
   948         raise error.ProgrammingError('should not be called')
   955         raise error.ProgrammingError(b'should not be called')
   949 
   956 
   950     args = {}
   957     args = {}
   951 
   958 
   952     # The client advertises support for newer protocols by adding an
   959     # The client advertises support for newer protocols by adding an
   953     # X-HgUpgrade-* header with a list of supported APIs and an
   960     # X-HgUpgrade-* header with a list of supported APIs and an
   954     # X-HgProto-* header advertising which serializing formats it supports.
   961     # X-HgProto-* header advertising which serializing formats it supports.
   955     # We only support the HTTP version 2 transport and CBOR responses for
   962     # We only support the HTTP version 2 transport and CBOR responses for
   956     # now.
   963     # now.
   957     advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
   964     advertisev2 = ui.configbool(b'experimental', b'httppeer.advertise-v2')
   958 
   965 
   959     if advertisev2:
   966     if advertisev2:
   960         args['headers'] = {
   967         args[b'headers'] = {
   961             r'X-HgProto-1': r'cbor',
   968             r'X-HgProto-1': r'cbor',
   962         }
   969         }
   963 
   970 
   964         args['headers'].update(
   971         args[b'headers'].update(
   965             encodevalueinheaders(
   972             encodevalueinheaders(
   966                 ' '.join(sorted(API_PEERS)),
   973                 b' '.join(sorted(API_PEERS)),
   967                 'X-HgUpgrade',
   974                 b'X-HgUpgrade',
   968                 # We don't know the header limit this early.
   975                 # We don't know the header limit this early.
   969                 # So make it small.
   976                 # So make it small.
   970                 1024,
   977                 1024,
   971             )
   978             )
   972         )
   979         )
   973 
   980 
   974     req, requrl, qs = makev1commandrequest(
   981     req, requrl, qs = makev1commandrequest(
   975         ui, requestbuilder, caps, capable, url, 'capabilities', args
   982         ui, requestbuilder, caps, capable, url, b'capabilities', args
   976     )
   983     )
   977     resp = sendrequest(ui, opener, req)
   984     resp = sendrequest(ui, opener, req)
   978 
   985 
   979     # The server may redirect us to the repo root, stripping the
   986     # The server may redirect us to the repo root, stripping the
   980     # ?cmd=capabilities query string from the URL. The server would likely
   987     # ?cmd=capabilities query string from the URL. The server would likely
   992         respurl, ct, resp = parsev1commandresponse(
   999         respurl, ct, resp = parsev1commandresponse(
   993             ui, url, requrl, qs, resp, compressible=False, allowcbor=advertisev2
  1000             ui, url, requrl, qs, resp, compressible=False, allowcbor=advertisev2
   994         )
  1001         )
   995     except RedirectedRepoError as e:
  1002     except RedirectedRepoError as e:
   996         req, requrl, qs = makev1commandrequest(
  1003         req, requrl, qs = makev1commandrequest(
   997             ui, requestbuilder, caps, capable, e.respurl, 'capabilities', args
  1004             ui, requestbuilder, caps, capable, e.respurl, b'capabilities', args
   998         )
  1005         )
   999         resp = sendrequest(ui, opener, req)
  1006         resp = sendrequest(ui, opener, req)
  1000         respurl, ct, resp = parsev1commandresponse(
  1007         respurl, ct, resp = parsev1commandresponse(
  1001             ui, url, requrl, qs, resp, compressible=False, allowcbor=advertisev2
  1008             ui, url, requrl, qs, resp, compressible=False, allowcbor=advertisev2
  1002         )
  1009         )
  1004     try:
  1011     try:
  1005         rawdata = resp.read()
  1012         rawdata = resp.read()
  1006     finally:
  1013     finally:
  1007         resp.close()
  1014         resp.close()
  1008 
  1015 
  1009     if not ct.startswith('application/mercurial-'):
  1016     if not ct.startswith(b'application/mercurial-'):
  1010         raise error.ProgrammingError('unexpected content-type: %s' % ct)
  1017         raise error.ProgrammingError(b'unexpected content-type: %s' % ct)
  1011 
  1018 
  1012     if advertisev2:
  1019     if advertisev2:
  1013         if ct == 'application/mercurial-cbor':
  1020         if ct == b'application/mercurial-cbor':
  1014             try:
  1021             try:
  1015                 info = cborutil.decodeall(rawdata)[0]
  1022                 info = cborutil.decodeall(rawdata)[0]
  1016             except cborutil.CBORDecodeError:
  1023             except cborutil.CBORDecodeError:
  1017                 raise error.Abort(
  1024                 raise error.Abort(
  1018                     _('error decoding CBOR from remote server'),
  1025                     _(b'error decoding CBOR from remote server'),
  1019                     hint=_(
  1026                     hint=_(
  1020                         'try again and consider contacting '
  1027                         b'try again and consider contacting '
  1021                         'the server operator'
  1028                         b'the server operator'
  1022                     ),
  1029                     ),
  1023                 )
  1030                 )
  1024 
  1031 
  1025         # We got a legacy response. That's fine.
  1032         # We got a legacy response. That's fine.
  1026         elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
  1033         elif ct in (b'application/mercurial-0.1', b'application/mercurial-0.2'):
  1027             info = {'v1capabilities': set(rawdata.split())}
  1034             info = {b'v1capabilities': set(rawdata.split())}
  1028 
  1035 
  1029         else:
  1036         else:
  1030             raise error.RepoError(
  1037             raise error.RepoError(
  1031                 _('unexpected response type from server: %s') % ct
  1038                 _(b'unexpected response type from server: %s') % ct
  1032             )
  1039             )
  1033     else:
  1040     else:
  1034         info = {'v1capabilities': set(rawdata.split())}
  1041         info = {b'v1capabilities': set(rawdata.split())}
  1035 
  1042 
  1036     return respurl, info
  1043     return respurl, info
  1037 
  1044 
  1038 
  1045 
  1039 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
  1046 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
  1046     It exists as an argument so extensions can override the default.
  1053     It exists as an argument so extensions can override the default.
  1047     """
  1054     """
  1048     u = util.url(path)
  1055     u = util.url(path)
  1049     if u.query or u.fragment:
  1056     if u.query or u.fragment:
  1050         raise error.Abort(
  1057         raise error.Abort(
  1051             _('unsupported URL component: "%s"') % (u.query or u.fragment)
  1058             _(b'unsupported URL component: "%s"') % (u.query or u.fragment)
  1052         )
  1059         )
  1053 
  1060 
  1054     # urllib cannot handle URLs with embedded user or passwd.
  1061     # urllib cannot handle URLs with embedded user or passwd.
  1055     url, authinfo = u.authinfo()
  1062     url, authinfo = u.authinfo()
  1056     ui.debug('using %s\n' % url)
  1063     ui.debug(b'using %s\n' % url)
  1057 
  1064 
  1058     opener = opener or urlmod.opener(ui, authinfo)
  1065     opener = opener or urlmod.opener(ui, authinfo)
  1059 
  1066 
  1060     respurl, info = performhandshake(ui, url, opener, requestbuilder)
  1067     respurl, info = performhandshake(ui, url, opener, requestbuilder)
  1061 
  1068 
  1066     # example, the caller could say "I want a peer that does X." It's quite
  1073     # example, the caller could say "I want a peer that does X." It's quite
  1067     # possible that not all peers would do that. Since we know the service
  1074     # possible that not all peers would do that. Since we know the service
  1068     # capabilities, we could filter out services not meeting the
  1075     # capabilities, we could filter out services not meeting the
  1069     # requirements. Possibly by consulting the interfaces defined by the
  1076     # requirements. Possibly by consulting the interfaces defined by the
  1070     # peer type.
  1077     # peer type.
  1071     apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
  1078     apipeerchoices = set(info.get(b'apis', {}).keys()) & set(API_PEERS.keys())
  1072 
  1079 
  1073     preferredchoices = sorted(
  1080     preferredchoices = sorted(
  1074         apipeerchoices, key=lambda x: API_PEERS[x]['priority'], reverse=True
  1081         apipeerchoices, key=lambda x: API_PEERS[x][b'priority'], reverse=True
  1075     )
  1082     )
  1076 
  1083 
  1077     for service in preferredchoices:
  1084     for service in preferredchoices:
  1078         apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
  1085         apipath = b'%s/%s' % (info[b'apibase'].rstrip(b'/'), service)
  1079 
  1086 
  1080         return API_PEERS[service]['init'](
  1087         return API_PEERS[service][b'init'](
  1081             ui, respurl, apipath, opener, requestbuilder, info['apis'][service]
  1088             ui, respurl, apipath, opener, requestbuilder, info[b'apis'][service]
  1082         )
  1089         )
  1083 
  1090 
  1084     # Failed to construct an API peer. Fall back to legacy.
  1091     # Failed to construct an API peer. Fall back to legacy.
  1085     return httppeer(
  1092     return httppeer(
  1086         ui, path, respurl, opener, requestbuilder, info['v1capabilities']
  1093         ui, path, respurl, opener, requestbuilder, info[b'v1capabilities']
  1087     )
  1094     )
  1088 
  1095 
  1089 
  1096 
  1090 def instance(ui, path, create, intents=None, createopts=None):
  1097 def instance(ui, path, create, intents=None, createopts=None):
  1091     if create:
  1098     if create:
  1092         raise error.Abort(_('cannot create new http repository'))
  1099         raise error.Abort(_(b'cannot create new http repository'))
  1093     try:
  1100     try:
  1094         if path.startswith('https:') and not urlmod.has_https:
  1101         if path.startswith(b'https:') and not urlmod.has_https:
  1095             raise error.Abort(
  1102             raise error.Abort(
  1096                 _('Python support for SSL and HTTPS ' 'is not installed')
  1103                 _(b'Python support for SSL and HTTPS ' b'is not installed')
  1097             )
  1104             )
  1098 
  1105 
  1099         inst = makepeer(ui, path)
  1106         inst = makepeer(ui, path)
  1100 
  1107 
  1101         return inst
  1108         return inst
  1102     except error.RepoError as httpexception:
  1109     except error.RepoError as httpexception:
  1103         try:
  1110         try:
  1104             r = statichttprepo.instance(ui, "static-" + path, create)
  1111             r = statichttprepo.instance(ui, b"static-" + path, create)
  1105             ui.note(_('(falling back to static-http)\n'))
  1112             ui.note(_(b'(falling back to static-http)\n'))
  1106             return r
  1113             return r
  1107         except error.RepoError:
  1114         except error.RepoError:
  1108             raise httpexception  # use the original http RepoError instead
  1115             raise httpexception  # use the original http RepoError instead