diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,11 @@
+git-annex (10.20260525) upstream; urgency=medium
+
+  * Fix reversion introduced in 10.20260421 that caused commands
+    like git-annex copy to stop early due to SIGPIPE.
+  * p2phttp: Avoid hanging after HEAD request.
+
+ -- Joey Hess <id@joeyh.name>  Tue, 26 May 2026 07:49:42 -0400
+
 git-annex (10.20260520) upstream; urgency=medium
 
   * Behavior change: git-annex sync now defaults to syncing content,
diff --git a/Messages.hs b/Messages.hs
--- a/Messages.hs
+++ b/Messages.hs
@@ -65,9 +65,7 @@
 import System.Exit
 import qualified Control.Monad.Catch as M
 import Data.String
-#ifndef mingw32_HOST_OS
-import System.Posix.Signals
-#endif
+import GHC.IO.Exception
 
 import Common
 import Types
@@ -355,26 +353,26 @@
 				(\v -> putMVar l v >> cleanup)
 				(const $ run a)
 
-{- Catch all (non-async and not ExitCode) exceptions and display, 
- - sanitizing any control characters in the exceptions.
+{- Catch all (non-async) exceptions and display, sanitizing any control
+ - characters in the exceptions.
  -
  - Should only be used at topmost level.
  -}
 sanitizeTopLevelExceptionMessages :: IO a -> IO a
 sanitizeTopLevelExceptionMessages a = do
-#ifndef mingw32_HOST_OS
-	-- By default ghc Ignores sigPIPE, and then does not display
-	-- exceptions like <stdout>: hFlush: resource vanished (Broken pipe)
-	--
-	-- Since this would display such exceptions, instead restore the
-	-- Default sigPIPE behavior, which is for the program to
-	-- immediately exit.
-	void $ installHandler sigPIPE Default Nothing
-#endif
-	a `catches`
-		((M.Handler (\ (e :: ExitCode) -> throwM e)) : nonAsyncHandler go)
+	a `catches` (ignoreExitCode : ignoreBrokenPipe : nonAsyncHandler go)
   where
 	go e = giveup $ show e
+
+	-- Propigate ExitCode exceptions so the program exits with the code
+	-- as usual.
+	ignoreExitCode = M.Handler $ \(e :: ExitCode) -> throwM e
+
+	-- ghc's default toplevel exception handler avoids displaying
+	-- IOExceptions caused by SIGPIPE. So, rethrow the IOException
+	-- to it. But sanitize the exception description first.
+	ignoreBrokenPipe = M.Handler $ \(e :: IOException) ->
+		throwM $ e { ioe_description = safeOutput (ioe_description e) }
 
 {- Used to only run an action that displays a message after the specified
  - number of steps. This is useful when performing an action that can
diff --git a/P2P/Http/State.hs b/P2P/Http/State.hs
--- a/P2P/Http/State.hs
+++ b/P2P/Http/State.hs
@@ -440,15 +440,18 @@
 	serverrunst <- mkServerRunState connparams
 	asyncworker <- async $
 		startworker serverrunst serverconn
-	let releaseconn = atomically $ void $ tryPutTMVar relv $
+	let releaseconn closeit = atomically $ void $ tryPutTMVar relv $ do
+		when closeit $ do
+			liftIO $ closeConnection clientconn
+			liftIO $ closeConnection serverconn
 		liftIO $ wait asyncworker
 			>>= either throwM return
 	return $ Right $ P2PConnectionPair
 		{ clientRunState = clientrunst
 		, clientP2PConnection = clientconn
 		, serverP2PConnection = serverconn
-		, releaseP2PConnection = releaseconn
-		, closeP2PConnection = releaseconn
+		, releaseP2PConnection = releaseconn False
+		, closeP2PConnection = releaseconn True
 		}
 
 mkP2PConnectionPair
diff --git a/git-annex.cabal b/git-annex.cabal
--- a/git-annex.cabal
+++ b/git-annex.cabal
@@ -1,5 +1,5 @@
 Name: git-annex
-Version: 10.20260520
+Version: 10.20260525
 Cabal-Version: 1.12
 License: AGPL-3
 Maintainer: Joey Hess <id@joeyh.name>
