diff --git a/mercurial/hgweb/hgwebdir_mod.py b/mercurial/hgweb/hgwebdir_mod.py
--- a/mercurial/hgweb/hgwebdir_mod.py
+++ b/mercurial/hgweb/hgwebdir_mod.py
@@ -428,7 +428,10 @@ class hgwebdir(object):
                                 uenv.iteritems()}
                     req = requestmod.parserequestfromenv(
                         uenv, reponame=virtualrepo,
-                        altbaseurl=self.ui.config('web', 'baseurl'))
+                        altbaseurl=self.ui.config('web', 'baseurl'),
+                        # Reuse wrapped body file object otherwise state
+                        # tracking can get confused.
+                        bodyfh=req.bodyfh)
                     try:
                         # ensure caller gets private copy of ui
                         repo = hg.repository(self.ui.copy(), real)
diff --git a/mercurial/hgweb/request.py b/mercurial/hgweb/request.py
--- a/mercurial/hgweb/request.py
+++ b/mercurial/hgweb/request.py
@@ -124,7 +124,7 @@ class parsedrequest(object):
     # WSGI environment dict, unmodified.
     rawenv = attr.ib()
 
-def parserequestfromenv(env, reponame=None, altbaseurl=None):
+def parserequestfromenv(env, reponame=None, altbaseurl=None, bodyfh=None):
     """Parse URL components from environment variables.
 
     WSGI defines request attributes via environment variables. This function
@@ -144,6 +144,9 @@ def parserequestfromenv(env, reponame=No
     if the request were to ``http://myserver:9000/prefix/rev/@``. In other
     words, ``wsgi.url_scheme``, ``SERVER_NAME``, ``SERVER_PORT``, and
     ``SCRIPT_NAME`` are all effectively replaced by components from this URL.
+
+    ``bodyfh`` can be used to specify a file object to read the request body
+    from. If not defined, ``wsgi.input`` from the environment dict is used.
     """
     # PEP 3333 defines the WSGI spec and is a useful reference for this code.
 
@@ -307,9 +310,10 @@ def parserequestfromenv(env, reponame=No
     if 'CONTENT_TYPE' in env and 'HTTP_CONTENT_TYPE' not in env:
         headers['Content-Type'] = env['CONTENT_TYPE']
 
-    bodyfh = env['wsgi.input']
-    if 'Content-Length' in headers:
-        bodyfh = util.cappedreader(bodyfh, int(headers['Content-Length']))
+    if bodyfh is None:
+        bodyfh = env['wsgi.input']
+        if 'Content-Length' in headers:
+            bodyfh = util.cappedreader(bodyfh, int(headers['Content-Length']))
 
     return parsedrequest(method=env['REQUEST_METHOD'],
                          url=fullurl, baseurl=baseurl,
diff --git a/tests/test-push-http.t b/tests/test-push-http.t
--- a/tests/test-push-http.t
+++ b/tests/test-push-http.t
@@ -380,3 +380,47 @@ Make phases updates work
 #endif
 
   $ cd ..
+
+Pushing via hgwebdir works
+
+  $ hg init hgwebdir
+  $ cd hgwebdir
+  $ echo 0 > a
+  $ hg -q commit -A -m initial
+  $ cd ..
+
+  $ cat > web.conf << EOF
+  > [paths]
+  > / = *
+  > [web]
+  > push_ssl = false
+  > allow_push = *
+  > EOF
+
+  $ hg serve --web-conf web.conf -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ hg clone http://localhost:$HGPORT/hgwebdir hgwebdir-local
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  new changesets 98a3f8f02ba7
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd hgwebdir-local
+  $ echo commit > a
+  $ hg commit -m 'local commit'
+
+  $ hg push
+  pushing to http://localhost:$HGPORT/hgwebdir
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+
+  $ killdaemons.py
+
+  $ cd ..