WAGON-541 add consistency to exception messages for all wagons so that clients ( ie. Maven ) reporting the Exceptions to users have good consistent contextual information. This means also report any custom status line reason phrases the servers involved might return.
diff --git a/wagon-provider-test/src/main/java/org/apache/maven/wagon/http/HttpWagonTestCase.java b/wagon-provider-test/src/main/java/org/apache/maven/wagon/http/HttpWagonTestCase.java
index 463431a..a90f8f4 100644
--- a/wagon-provider-test/src/main/java/org/apache/maven/wagon/http/HttpWagonTestCase.java
+++ b/wagon-provider-test/src/main/java/org/apache/maven/wagon/http/HttpWagonTestCase.java
@@ -25,6 +25,7 @@
 import org.apache.maven.wagon.StreamingWagonTestCase;
 import org.apache.maven.wagon.TransferFailedException;
 import org.apache.maven.wagon.Wagon;
+import org.apache.maven.wagon.WagonException;
 import org.apache.maven.wagon.authentication.AuthenticationInfo;
 import org.apache.maven.wagon.authorization.AuthorizationException;
 import org.apache.maven.wagon.proxy.ProxyInfo;
@@ -34,6 +35,7 @@
 import org.codehaus.plexus.util.FileUtils;
 import org.codehaus.plexus.util.IOUtil;
 import org.codehaus.plexus.util.StringUtils;
+import org.eclipse.jetty.http.HttpStatus;
 import org.eclipse.jetty.security.ConstraintMapping;
 import org.eclipse.jetty.security.ConstraintSecurityHandler;
 import org.eclipse.jetty.security.HashLoginService;
@@ -401,20 +403,25 @@
     {
         StreamingWagon wagon = (StreamingWagon) getWagon();
 
-        Server server = new Server(  );
-        StatusHandler handler = new StatusHandler();
-        handler.setStatusToReturn( status );
-        server.setHandler( handler );
-        addConnector( server );
+        Server server = createStatusServer( status );
         server.start();
 
-        wagon.connect( new Repository( "id", getRepositoryUrl( server ) ) );
+        String baseUrl = getRepositoryUrl( server );
+        String resourceName = "resource";
+        String serverReasonPhrase = HttpStatus.getCode( status ).getMessage();
+
+        wagon.connect( new Repository( "id", baseUrl ) );
 
         try
         {
             wagon.getToStream( "resource", new ByteArrayOutputStream() );
             fail();
         }
+        catch ( Exception e )
+        {
+            verifyWagonExceptionMessage( e, status, baseUrl + "/" + resourceName, serverReasonPhrase );
+            throw e;
+        }
         finally
         {
             wagon.disconnect();
@@ -523,11 +530,7 @@
     {
         StreamingWagon wagon = (StreamingWagon) getWagon();
 
-        Server server = new Server( );
-        StatusHandler handler = new StatusHandler();
-        handler.setStatusToReturn( status );
-        server.setHandler( handler );
-        addConnector( server );
+        Server server = createStatusServer( status );
         server.start();
 
         wagon.connect( new Repository( "id", getRepositoryUrl( server ) ) );
@@ -1593,6 +1596,16 @@
         return server;
     }
 
+    private Server createStatusServer( int status )
+    {
+        Server server = new Server( );
+        StatusHandler handler = new StatusHandler();
+        handler.setStatusToReturn( status );
+        server.setHandler( handler );
+        addConnector( server );
+        return server;
+    }
+
 
     private String writeTestFile( File parent, String child, String compressionType )
         throws IOException
@@ -1756,11 +1769,7 @@
     {
         StreamingWagon wagon = (StreamingWagon) getWagon();
 
-        Server server = new Server( );
-        StatusHandler handler = new StatusHandler();
-        handler.setStatusToReturn( status );
-        server.setHandler( handler );
-        addConnector( server );
+        Server server = createStatusServer( status );
         server.start();
 
         wagon.connect( new Repository( "id", getRepositoryUrl( server ) ) );
@@ -1769,11 +1778,20 @@
         tempFile.deleteOnExit();
         FileUtils.fileWrite( tempFile.getAbsolutePath(), "content" );
 
+        String baseUrl = getRepositoryUrl( server );
+        String resourceName = "resource";
+        String serverReasonPhrase = HttpStatus.getCode( status ).getMessage();
+
         try
         {
-            wagon.put( tempFile, "resource" );
+            wagon.put( tempFile, resourceName );
             fail();
         }
+        catch ( Exception e )
+        {
+            verifyWagonExceptionMessage( e, status, baseUrl + "/" + resourceName, serverReasonPhrase );
+            throw e;
+        }
         finally
         {
             wagon.disconnect();
@@ -2281,4 +2299,85 @@
             return sb.toString();
         }
     }
+
+    /**
+     * Verify a WagonException message contains required format and context based on the status code we expected to
+     * trigger it in the first place.
+     * <p>
+     * This implementation represents the most desired assertions, but HttpWagonTestCase sub-classes could override
+     * this method if a specific wagon representation makes it impossible to meet these assertions.
+     *
+     * @param e               an instance of {@link WagonException}
+     * @param forStatusCode   the response status code that triggered the exception
+     * @param forUrl          the url that triggered the exception
+     * @param forReasonPhrase the optional status line reason phrase the server returned
+     */
+    protected void verifyWagonExceptionMessage( Exception e, int forStatusCode, String forUrl, String forReasonPhrase )
+    {
+        // TODO: handle AuthenticationException for Wagon.connect() calls
+        assertNotNull( e );
+        try
+        {
+            assertTrue( "only verify instances of WagonException", e instanceof WagonException );
+
+            String reasonPhrase;
+            String assertMessageForBadMessage = "exception message not described properly";
+            switch ( forStatusCode )
+            {
+                case HttpServletResponse.SC_NOT_FOUND:
+                    // TODO: add test for 410: Gone?
+                    assertTrue( "404 not found response should throw ResourceDoesNotExistException",
+                            e instanceof ResourceDoesNotExistException );
+                    reasonPhrase = StringUtils.isEmpty( forReasonPhrase ) ? " Not Found" : ( " " + forReasonPhrase );
+                    assertEquals( assertMessageForBadMessage, "Resource missing at " + forUrl + " 404"
+                            + reasonPhrase, e.getMessage() );
+                    break;
+
+                case HttpServletResponse.SC_UNAUTHORIZED:
+                    // FIXME assumes Wagon.get()/put() returning 401 instead of Wagon.connect()
+                    assertTrue( "401 Unauthorized should throw AuthorizationException since "
+                                    + " AuthenticationException is not explicitly declared as thrown from wagon "
+                                    + "methods",
+                            e instanceof AuthorizationException );
+                    reasonPhrase = StringUtils.isEmpty( forReasonPhrase ) ? " Unauthorized" : ( " " + forReasonPhrase );
+                    assertEquals( assertMessageForBadMessage, "Authentication failed for " + forUrl + " 401"
+                            + reasonPhrase, e.getMessage() );
+                    break;
+
+                case HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED:
+                    assertTrue( "407 Proxy authentication required should throw AuthorizationException",
+                            e instanceof AuthorizationException );
+                    reasonPhrase = StringUtils.isEmpty( forReasonPhrase ) ? " Proxy Authentication Required"
+                            : ( " " + forReasonPhrase );
+                    assertEquals( assertMessageForBadMessage, "HTTP proxy server authentication failed for "
+                            + forUrl + " 407" + reasonPhrase, e.getMessage() );
+                    break;
+
+                case HttpServletResponse.SC_FORBIDDEN:
+                    assertTrue( "403 Forbidden should throw AuthorizationException",
+                            e instanceof AuthorizationException );
+                    reasonPhrase = StringUtils.isEmpty( forReasonPhrase ) ? " Forbidden" : ( " " + forReasonPhrase );
+                    assertEquals( assertMessageForBadMessage, "Authorization failed for " + forUrl + " 403"
+                            + reasonPhrase, e.getMessage() );
+                    break;
+
+                default:
+                    assertTrue( "transfer failures should at least be wrapped in a TransferFailedException", e
+                                    instanceof TransferFailedException );
+                    assertTrue( "expected status code for transfer failures should be >= 400",
+                            forStatusCode >= HttpServletResponse.SC_BAD_REQUEST );
+                    reasonPhrase = forReasonPhrase == null ? "" : " " + forReasonPhrase;
+                    assertEquals( assertMessageForBadMessage, "Transfer failed for " + forUrl + " "
+                            + forStatusCode + reasonPhrase, e.getMessage() );
+                    break;
+            }
+        }
+        catch ( AssertionError assertionError )
+        {
+            logger.error( "Exception which failed assertions: ", e );
+            throw assertionError;
+        }
+
+    }
+
 }
diff --git a/wagon-providers/wagon-http-lightweight/src/main/java/org/apache/maven/wagon/providers/http/LightweightHttpWagon.java b/wagon-providers/wagon-http-lightweight/src/main/java/org/apache/maven/wagon/providers/http/LightweightHttpWagon.java
index 50fc28a..d0f32eb 100644
--- a/wagon-providers/wagon-http-lightweight/src/main/java/org/apache/maven/wagon/providers/http/LightweightHttpWagon.java
+++ b/wagon-providers/wagon-http-lightweight/src/main/java/org/apache/maven/wagon/providers/http/LightweightHttpWagon.java
@@ -50,9 +50,17 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.zip.DeflaterInputStream;
 import java.util.zip.GZIPInputStream;
 
+import static java.lang.Integer.parseInt;
+import static org.apache.maven.wagon.shared.http.HttpMessageUtils.UNKNOWN_STATUS_CODE;
+import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatAuthorizationMessage;
+import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage;
+import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferFailedMessage;
+
 /**
  * LightweightHttpWagon, using JDK's HttpURLConnection.
  *
@@ -69,6 +77,9 @@
 
     private Proxy proxy = Proxy.NO_PROXY;
 
+    private static final Pattern IOEXCEPTION_MESSAGE_PATTERN = Pattern.compile( "Server returned HTTP response code: "
+            + "(\\d\\d\\d) for URL: (.*)" );
+
     public static final int MAX_REDIRECTS = 10;
 
     /**
@@ -105,20 +116,46 @@
         Resource resource = inputData.getResource();
 
         String visitingUrl = buildUrl( resource );
-        try
+
+        List<String> visitedUrls = new ArrayList<>();
+
+        for ( int redirectCount = 0; redirectCount < MAX_REDIRECTS; redirectCount++ )
         {
-            List<String> visitedUrls = new ArrayList<String>();
-
-            for ( int redirectCount = 0; redirectCount < MAX_REDIRECTS; redirectCount++ )
+            if ( visitedUrls.contains( visitingUrl ) )
             {
-                if ( visitedUrls.contains( visitingUrl ) )
-                {
-                    throw new TransferFailedException( "Cyclic http redirect detected. Aborting! " + visitingUrl );
-                }
-                visitedUrls.add( visitingUrl );
+                // TODO add a test for this message
+                throw new TransferFailedException( "Cyclic http redirect detected. Aborting! " + visitingUrl );
+            }
+            visitedUrls.add( visitingUrl );
 
-                URL url = new URL( visitingUrl );
-                HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection( this.proxy );
+            URL url = null;
+            try
+            {
+                url = new URL( visitingUrl );
+            }
+            catch ( MalformedURLException e )
+            {
+                // TODO add test for this
+                throw new ResourceDoesNotExistException( "Invalid repository URL: " + e.getMessage(), e );
+            }
+
+            HttpURLConnection urlConnection = null;
+
+            try
+            {
+                urlConnection = ( HttpURLConnection ) url.openConnection( this.proxy );
+            }
+            catch ( IOException e )
+            {
+                // TODO: add test for this
+                String message = formatTransferFailedMessage( visitingUrl, UNKNOWN_STATUS_CODE,
+                        null, getProxyInfo() );
+                // TODO include e.getMessage appended to main message?
+                throw new TransferFailedException( message, e );
+            }
+
+            try
+            {
 
                 urlConnection.setRequestProperty( "Accept-Encoding", "gzip,deflate" );
                 if ( !useCache )
@@ -130,13 +167,16 @@
 
                 // TODO: handle all response codes
                 int responseCode = urlConnection.getResponseCode();
+                String reasonPhrase = urlConnection.getResponseMessage();
+
                 if ( responseCode == HttpURLConnection.HTTP_FORBIDDEN
-                    || responseCode == HttpURLConnection.HTTP_UNAUTHORIZED )
+                        || responseCode == HttpURLConnection.HTTP_UNAUTHORIZED )
                 {
-                    throw new AuthorizationException( "Access denied to: " + buildUrl( resource ) );
+                    throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ),
+                            responseCode, reasonPhrase, getProxyInfo() ) );
                 }
                 if ( responseCode == HttpURLConnection.HTTP_MOVED_PERM
-                    || responseCode == HttpURLConnection.HTTP_MOVED_TEMP )
+                        || responseCode == HttpURLConnection.HTTP_MOVED_TEMP )
                 {
                     visitingUrl = urlConnection.getHeaderField( "Location" );
                     continue;
@@ -158,27 +198,22 @@
                 resource.setLastModified( urlConnection.getLastModified() );
                 resource.setContentLength( urlConnection.getContentLength() );
                 break;
+
             }
-        }
-        catch ( MalformedURLException e )
-        {
-            throw new ResourceDoesNotExistException( "Invalid repository URL: " + e.getMessage(), e );
-        }
-        catch ( FileNotFoundException e )
-        {
-            throw new ResourceDoesNotExistException( "Unable to locate resource in repository", e );
-        }
-        catch ( IOException e )
-        {
-            StringBuilder message = new StringBuilder( "Error transferring file: " );
-            message.append( e.getMessage() );
-            message.append( " from " + visitingUrl );
-            if ( getProxyInfo() != null && getProxyInfo().getHost() != null )
+            catch ( FileNotFoundException e )
             {
-                message.append( " with proxyInfo " ).append( getProxyInfo().toString() );
+                // this could be 404 Not Found or 410 Gone - we don't have access to which it was.
+                // TODO: 2019-10-03 url used should list all visited/redirected urls, not just the original
+                throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ),
+                        UNKNOWN_STATUS_CODE, null, getProxyInfo() ), e );
             }
-            throw new TransferFailedException( message.toString(), e );
+            catch ( IOException originalIOException )
+            {
+                throw convertHttpUrlConnectionException( originalIOException, urlConnection, buildUrl( resource ) );
+            }
+
         }
+
     }
 
     private void addHeaders( HttpURLConnection urlConnection )
@@ -229,6 +264,7 @@
     {
         try
         {
+            String reasonPhrase = putConnection.getResponseMessage();
             int statusCode = putConnection.getResponseCode();
 
             switch ( statusCode )
@@ -240,23 +276,25 @@
                 case HttpURLConnection.HTTP_NO_CONTENT: // 204
                     break;
 
+                // TODO: handle 401 explicitly?
                 case HttpURLConnection.HTTP_FORBIDDEN:
-                    throw new AuthorizationException( "Access denied to: " + buildUrl( resource ) );
+                    throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), statusCode,
+                            reasonPhrase, getProxyInfo() ) );
 
                 case HttpURLConnection.HTTP_NOT_FOUND:
-                    throw new ResourceDoesNotExistException( "File: " + buildUrl( resource ) + " does not exist" );
+                    throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ),
+                            statusCode, reasonPhrase, getProxyInfo() ) );
 
-                    // add more entries here
+                // add more entries here
                 default:
-                    throw new TransferFailedException(
-                        "Failed to transfer file: " + buildUrl( resource ) + ". Return code is: " + statusCode );
+                    throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ),
+                            statusCode, reasonPhrase, getProxyInfo() ) ) ;
             }
         }
         catch ( IOException e )
         {
             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
-
-            throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
+            throw convertHttpUrlConnectionException( e, putConnection, buildUrl( resource ) );
         }
     }
 
@@ -474,4 +512,73 @@
     {
         this.authenticator = authenticator;
     }
+
+    /**
+     * Convert the IOException that is thrown for most transfer errors that HttpURLConnection encounters to the
+     * equivalent {@link TransferFailedException}.
+     * <p>
+     * Details are extracted from the error stream if possible, either directly or indirectly by way of supporting
+     * accessors. The returned exception will include the passed IOException as a cause and a message that is as
+     * descriptive as possible.
+     *
+     * @param originalIOException an IOException thrown from an HttpURLConnection operation
+     * @param urlConnection       instance that triggered the IOException
+     * @param url                 originating url that triggered the IOException
+     * @return exception that is representative of the original cause
+     */
+    private TransferFailedException convertHttpUrlConnectionException( IOException originalIOException,
+                                                                       HttpURLConnection urlConnection,
+                                                                       String url )
+    {
+        // javadoc of HttpUrlConnection, HTTP transfer errors throw IOException
+        // In that case, one may attempt to get the status code and reason phrase
+        // from the errorstream. We do this, but by way of the following code path
+        // getResponseCode()/getResponseMessage() - calls -> getHeaderFields()
+        // getHeaderFields() - calls -> getErrorStream()
+        try
+        {
+            // call getResponseMessage first since impl calls getResponseCode as part of that anyways
+            String errorResponseMessage = urlConnection.getResponseMessage(); // may be null
+            int errorResponseCode = urlConnection.getResponseCode(); // may be -1 if the code cannot be discerned
+            String message = formatTransferFailedMessage( url, errorResponseCode, errorResponseMessage,
+                    getProxyInfo() );
+            return new TransferFailedException( message, originalIOException );
+
+        }
+        catch ( IOException errorStreamException )
+        {
+            // there was a problem using the standard methods, need to fall back to other options
+        }
+
+        // Attempt to parse the status code and URL which can be included in an IOException message
+        // https://github.com/AdoptOpenJDK/openjdk-jdk11/blame/999dbd4192d0f819cb5224f26e9e7fa75ca6f289/src/java
+        // .base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#L1911L1913
+        String ioMsg = originalIOException.getMessage();
+        if ( ioMsg != null )
+        {
+            Matcher matcher = IOEXCEPTION_MESSAGE_PATTERN.matcher( ioMsg );
+            if ( matcher.matches() )
+            {
+                String codeStr = matcher.group( 1 );
+                String urlStr = matcher.group( 2 );
+
+                int code = UNKNOWN_STATUS_CODE;
+                try
+                {
+                    code = parseInt( codeStr );
+                }
+                catch ( NumberFormatException nfe )
+                {
+                    // if here there is a regex problem
+                }
+
+                String message = formatTransferFailedMessage( urlStr, code, null, getProxyInfo() );
+                return new TransferFailedException( message, originalIOException );
+            }
+        }
+
+        String message = formatTransferFailedMessage( url, UNKNOWN_STATUS_CODE, null, getProxyInfo() );
+        return new TransferFailedException( message, originalIOException );
+    }
+
 }
diff --git a/wagon-providers/wagon-http-lightweight/src/test/java/org/apache/maven/wagon/providers/http/LightweightHttpWagonTest.java b/wagon-providers/wagon-http-lightweight/src/test/java/org/apache/maven/wagon/providers/http/LightweightHttpWagonTest.java
index 06929c4..1a7de5e 100644
--- a/wagon-providers/wagon-http-lightweight/src/test/java/org/apache/maven/wagon/providers/http/LightweightHttpWagonTest.java
+++ b/wagon-providers/wagon-http-lightweight/src/test/java/org/apache/maven/wagon/providers/http/LightweightHttpWagonTest.java
@@ -19,9 +19,17 @@
  * under the License.
  */
 
+import org.apache.maven.wagon.ResourceDoesNotExistException;
 import org.apache.maven.wagon.StreamingWagon;
+import org.apache.maven.wagon.TransferFailedException;
+import org.apache.maven.wagon.WagonException;
+import org.apache.maven.wagon.authorization.AuthorizationException;
 import org.apache.maven.wagon.http.HttpWagonTestCase;
+import org.codehaus.plexus.util.StringUtils;
 
+import javax.servlet.http.HttpServletResponse;
+import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.Properties;
 
 /**
@@ -63,4 +71,102 @@
     {
         return false;
     }
+
+    @Override
+    protected void verifyWagonExceptionMessage(Exception e, int forStatusCode, String forUrl, String forReasonPhrase)
+    {
+
+        // HttpUrlConnection prevents direct API access to the response code or reasonPhrase for any
+        // status code >= 400. So all we can do is check WagonException wraps the HttpUrlConnection
+        // thrown IOException / FileNotFoundException as a cause, if cause is not null
+
+        assertNotNull( e );
+        try
+        {
+            assertTrue( "only verify instances of WagonException", e instanceof WagonException );
+
+            String assertMessageForBadMessage = "exception message not described properly: ";
+            switch ( forStatusCode )
+            {
+                case HttpServletResponse.SC_GONE:
+                case HttpServletResponse.SC_NOT_FOUND:
+                    assertTrue( "404 or 410 should throw ResourceDoesNotExistException",
+                            e instanceof ResourceDoesNotExistException );
+
+                    if ( e.getCause() != null )
+                    {
+                        assertTrue( "ResourceDoesNotExistException should have the expected cause",
+                                e.getCause() instanceof FileNotFoundException );
+                        // the status code and reason phrase cannot always be learned due to implementation limitations
+                        // which means the message may not include them
+                        assertEquals( assertMessageForBadMessage, "Resource missing at " + forUrl, e.getMessage() );
+                    }
+                    else
+                    {
+                        assertEquals( assertMessageForBadMessage, "Resource missing at " + forUrl
+                                + " " + forStatusCode + " " + forReasonPhrase, e.getMessage() );
+                    }
+
+                    break;
+
+                case HttpServletResponse.SC_FORBIDDEN:
+                    assertTrue( "403 Forbidden throws AuthorizationException",
+                            e instanceof AuthorizationException );
+
+                    assertEquals( assertMessageForBadMessage, "Authorization failed for " + forUrl + " 403"
+                            + ( StringUtils.isEmpty( forReasonPhrase ) ? " Forbidden" : ( " " + forReasonPhrase ) ),
+                            e.getMessage() );
+                    break;
+
+                case HttpServletResponse.SC_UNAUTHORIZED:
+                    assertTrue( "401 Unauthorized throws AuthorizationException",
+                            e instanceof AuthorizationException );
+
+                    assertEquals( assertMessageForBadMessage, "Authentication failed for " + forUrl + " 401"
+                                    + ( StringUtils.isEmpty( forReasonPhrase ) ? " Unauthorized" :
+                                    ( " " + forReasonPhrase ) ),
+                            e.getMessage() );
+                    break;
+
+                default:
+                    assertTrue( "general exception must be TransferFailedException",
+                            e instanceof TransferFailedException );
+                    assertTrue( "expected status code for transfer failures should be >= 400, but none of "
+                                    + " the already handled codes",
+                            forStatusCode >= HttpServletResponse.SC_BAD_REQUEST );
+
+                    if( e.getCause() != null ){
+                        assertTrue("TransferFailedException should have the original cause for diagnosis",
+                                e.getCause() instanceof IOException );
+                    }
+
+                    // the status code and reason phrase cannot always be learned due to implementation limitations
+                    // so the message may not include them, but the implementation should use a consistent format
+                    assertTrue( "message should always include url",
+                            e.getMessage().startsWith( "Transfer failed for " + forUrl ) );
+
+                    if( e.getMessage().length() > ("Transfer failed for " + forUrl).length() )
+                    {
+                        assertTrue( "message should include url and status code",
+                                e.getMessage().startsWith( "Transfer failed for " + forUrl + " " + forStatusCode ) );
+                    }
+
+                    if( e.getMessage().length() > ("Transfer failed for " + forUrl + " " + forStatusCode).length() )
+                    {
+                        assertEquals( "message should include url and status code and reason phrase",
+                                "Transfer failed for " + forUrl + " " + forStatusCode + " " + forReasonPhrase,
+                                e.getMessage() );
+                    }
+
+                    break;
+            }
+
+        }
+        catch ( AssertionError assertionError )
+        {
+            logger.error( "Exception which failed assertions: ", e );
+            throw assertionError;
+        }
+    }
+
 }
diff --git a/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java
index 1d25664..9d3630b 100755
--- a/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java
+++ b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java
@@ -105,6 +105,11 @@
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
 
+import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatAuthorizationMessage;
+import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage;
+import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferDebugMessage;
+import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferFailedMessage;
+
 /**
  * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
  * @author <a href="mailto:james@atlassian.com">James William Dumay</a>
@@ -713,17 +718,9 @@
             CloseableHttpResponse response = execute( putMethod );
             try
             {
+                fireTransferDebug( formatTransferDebugMessage( url, response.getStatusLine().getStatusCode(),
+                        response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
                 int statusCode = response.getStatusLine().getStatusCode();
-                String reasonPhrase = response.getStatusLine().getReasonPhrase();
-                StringBuilder debugMessage = new StringBuilder();
-                debugMessage.append( url );
-                debugMessage.append( " -- " );
-                debugMessage.append( "status code: " ).append( statusCode );
-                if ( StringUtils.isNotEmpty( reasonPhrase ) )
-                {
-                    debugMessage.append( ", reason phrase: " ).append( reasonPhrase );
-                }
-                fireTransferDebug( debugMessage.toString() );
 
                 // Check that we didn't run out of retries.
                 switch ( statusCode )
@@ -741,20 +738,26 @@
                     case HttpStatus.SC_SEE_OTHER: // 303
                         put( resource, source, httpEntity, calculateRelocatedUrl( response ) );
                         return;
+                    //case HttpStatus.SC_UNAUTHORIZED:
                     case HttpStatus.SC_FORBIDDEN:
                         fireSessionConnectionRefused();
-                        throw new AuthorizationException( "Access denied to: " + url );
+                        throw new AuthorizationException( formatAuthorizationMessage( url,
+                                response.getStatusLine().getStatusCode(),
+                                response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
 
                     case HttpStatus.SC_NOT_FOUND:
-                        throw new ResourceDoesNotExistException( "File " + url + " does not exist" );
+                        throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( url,
+                                response.getStatusLine().getStatusCode(),
+                                response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
 
                     case SC_TOO_MANY_REQUESTS:
                         put( backoff( wait, url ), resource, source, httpEntity, url );
                         break;
                     //add more entries here
                     default:
-                        TransferFailedException e = new TransferFailedException(
-                            "Failed to transfer file " + url + " with status code " + statusCode );
+                        TransferFailedException e = new TransferFailedException( formatTransferFailedMessage( url,
+                                response.getStatusLine().getStatusCode(),
+                                response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
                         fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
                         throw e;
                 }
@@ -768,23 +771,11 @@
                 response.close();
             }
         }
-        catch ( IOException e )
+        catch ( IOException | HttpException | InterruptedException e )
         {
             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
 
-            throw new TransferFailedException( e.getMessage(), e );
-        }
-        catch ( HttpException e )
-        {
-            fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
-
-            throw new TransferFailedException( e.getMessage(), e );
-        }
-        catch ( InterruptedException e )
-        {
-            fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
-
-            throw new TransferFailedException( e.getMessage(), e );
+            throw new TransferFailedException( formatTransferFailedMessage( url, getProxyInfo() ), e );
         }
 
     }
@@ -831,14 +822,13 @@
                     case HttpStatus.SC_NOT_MODIFIED:
                         result = true;
                         break;
+
                     case HttpStatus.SC_FORBIDDEN:
-                        throw new AuthorizationException( "Access denied to: " + url );
-
                     case HttpStatus.SC_UNAUTHORIZED:
-                        throw new AuthorizationException( "Not authorized" );
-
                     case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
-                        throw new AuthorizationException( "Not authorized by proxy" );
+                        throw new AuthorizationException( formatAuthorizationMessage( url,
+                                response.getStatusLine().getStatusCode(),
+                                response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
 
                     case HttpStatus.SC_NOT_FOUND:
                         result = false;
@@ -849,8 +839,9 @@
 
                     //add more entries here
                     default:
-                        throw new TransferFailedException(
-                            "Failed to transfer file " + url + " with status code " + statusCode );
+                        throw new TransferFailedException( formatTransferFailedMessage( url,
+                                response.getStatusLine().getStatusCode(),
+                                response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
                 }
 
                 EntityUtils.consume( response.getEntity() );
@@ -861,17 +852,9 @@
                 response.close();
             }
         }
-        catch ( IOException e )
+        catch ( IOException | HttpException | InterruptedException e )
         {
-            throw new TransferFailedException( e.getMessage(), e );
-        }
-        catch ( HttpException e )
-        {
-            throw new TransferFailedException( e.getMessage(), e );
-        }
-        catch ( InterruptedException e )
-        {
-            throw new TransferFailedException( e.getMessage(), e );
+            throw new TransferFailedException( formatTransferFailedMessage( url, getProxyInfo() ), e );
         }
 
     }
@@ -1116,17 +1099,10 @@
         {
             CloseableHttpResponse response = execute( getMethod );
             closeable = response;
+
+            fireTransferDebug( formatTransferDebugMessage( url, response.getStatusLine().getStatusCode(),
+                    response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
             int statusCode = response.getStatusLine().getStatusCode();
-            String reasonPhrase = response.getStatusLine().getReasonPhrase();
-            StringBuilder debugMessage = new StringBuilder();
-            debugMessage.append( url );
-            debugMessage.append( " -- " );
-            debugMessage.append( "status code: " ).append( statusCode );
-            if ( StringUtils.isNotEmpty( reasonPhrase ) )
-            {
-                debugMessage.append( ", reason phrase: " ).append( reasonPhrase );
-            }
-            fireTransferDebug( debugMessage.toString() );
 
             switch ( statusCode )
             {
@@ -1136,20 +1112,19 @@
                 case HttpStatus.SC_NOT_MODIFIED:
                     // return, leaving last modified set to original value so getIfNewer should return unmodified
                     return;
+
                 case HttpStatus.SC_FORBIDDEN:
-                    fireSessionConnectionRefused();
-                    throw new AuthorizationException( "Access denied to: " + url );
-
                 case HttpStatus.SC_UNAUTHORIZED:
-                    fireSessionConnectionRefused();
-                    throw new AuthorizationException( "Not authorized" );
-
                 case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
                     fireSessionConnectionRefused();
-                    throw new AuthorizationException( "Not authorized by proxy" );
+                    throw new AuthorizationException( formatAuthorizationMessage( url,
+                            response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(),
+                            getProxyInfo() ) );
 
                 case HttpStatus.SC_NOT_FOUND:
-                    throw new ResourceDoesNotExistException( "File " + url + " does not exist" );
+                    throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( url,
+                            response.getStatusLine().getStatusCode(),
+                            response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
 
                 case SC_TOO_MANY_REQUESTS:
                     fillInputData( backoff( wait, url ), inputData );
@@ -1158,8 +1133,9 @@
                 // add more entries here
                 default:
                     cleanupGetTransfer( resource );
-                    TransferFailedException e = new TransferFailedException(
-                        "Failed to transfer file " + url + " with status code " + statusCode );
+                    TransferFailedException e = new TransferFailedException( formatTransferFailedMessage( url,
+                            response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(),
+                            getProxyInfo() ) );
                     fireTransferError( resource, e, TransferEvent.REQUEST_GET );
                     throw e;
             }
@@ -1199,23 +1175,11 @@
                 inputData.setInputStream( entity.getContent() );
             }
         }
-        catch ( IOException e )
+        catch ( IOException | HttpException | InterruptedException e )
         {
             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
 
-            throw new TransferFailedException( e.getMessage(), e );
-        }
-        catch ( HttpException e )
-        {
-            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
-
-            throw new TransferFailedException( e.getMessage(), e );
-        }
-        catch ( InterruptedException e )
-        {
-            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
-
-            throw new TransferFailedException( e.getMessage(), e );
+            throw new TransferFailedException( formatTransferFailedMessage( url, getProxyInfo() ), e );
         }
 
     }
diff --git a/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/HttpMessageUtils.java b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/HttpMessageUtils.java
new file mode 100644
index 0000000..2b490d0
--- /dev/null
+++ b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/HttpMessageUtils.java
@@ -0,0 +1,222 @@
+package org.apache.maven.wagon.shared.http;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.wagon.ResourceDoesNotExistException;
+import org.apache.maven.wagon.TransferFailedException;
+import org.apache.maven.wagon.authorization.AuthorizationException;
+import org.apache.maven.wagon.proxy.ProxyInfo;
+import org.codehaus.plexus.util.StringUtils;
+
+/**
+ * Helper for HTTP related messages.
+ *
+ * @since 3.3.4
+ */
+public class HttpMessageUtils
+{
+    // status codes here to avoid checkstyle magic number and not have have hard depend on non-wagon classes
+    private static final int SC_UNAUTHORIZED = 401;
+    private static final int SC_FORBIDDEN = 403;
+    private static final int SC_NOT_FOUND = 404;
+    private static final int SC_PROXY_AUTH_REQUIRED = 407;
+    private static final int SC_GONE = 410;
+
+    /**
+     * A HTTP status code used to indicate that the actual response status code is not known at time of message
+     * generation.
+     */
+    public static final int UNKNOWN_STATUS_CODE = -1;
+
+    /**
+     * Format a consistent HTTP transfer debug message combining url, status code, status line reason phrase and HTTP
+     * proxy server info.
+     * <p>
+     * Url will always be included in the message. A status code other than {@link #UNKNOWN_STATUS_CODE} will be
+     * included. A reason phrase will only be included if non-empty and status code is not {@link #UNKNOWN_STATUS_CODE}.
+     * Proxy information will only be included if not null.
+     *
+     * @param url          the required non-null URL associated with the message
+     * @param statusCode   an HTTP response status code
+     * @param reasonPhrase an HTTP status line reason phrase
+     * @param proxyInfo    proxy server used during the transfer, may be null if none used
+     * @return a formatted debug message combining the parameters of this method
+     * @throws NullPointerException if url is null
+     */
+    public static String formatTransferDebugMessage( String url, int statusCode, String reasonPhrase,
+                                                     ProxyInfo proxyInfo )
+    {
+        String msg = url;
+        if ( statusCode != UNKNOWN_STATUS_CODE )
+        {
+            msg += " -- status code: " + statusCode;
+            if ( StringUtils.isNotEmpty( reasonPhrase ) )
+            {
+                msg += ", reason phrase: " + reasonPhrase;
+            }
+        }
+        if ( proxyInfo != null )
+        {
+            msg += " -- " + proxyInfo.toString();
+        }
+        return msg;
+    }
+
+    /**
+     * Format a consistent message for HTTP related {@link TransferFailedException}.
+     * <p>
+     * This variation typically used in cases where there is no HTTP transfer response data to extract status code and
+     * reason phrase from. Equivalent to calling {@link #formatTransferFailedMessage(String, int, String, ProxyInfo)}
+     * with {@link #UNKNOWN_STATUS_CODE} and null reason phrase.
+     *
+     * @param url the URL associated with the message
+     * @param proxyInfo proxy server used during the transfer, may be null if none used
+     * @return a formatted failure message combining the parameters of this method
+     */
+    public static String formatTransferFailedMessage( String url, ProxyInfo proxyInfo )
+    {
+        return formatTransferFailedMessage( url, UNKNOWN_STATUS_CODE, null,
+                proxyInfo );
+    }
+
+    /**
+     * Format a consistent message for HTTP related {@link TransferFailedException}.
+     *
+     * @param url          the URL associated with the message
+     * @param statusCode   an HTTP response status code or {@link #UNKNOWN_STATUS_CODE}
+     * @param reasonPhrase an HTTP status line reason phrase or null if the reason phrase unknown
+     * @param proxyInfo    proxy server used during the transfer, may be null if none used
+     * @return a formatted failure message combining the parameters of this method
+     */
+    public static String formatTransferFailedMessage( String url, int statusCode, String reasonPhrase,
+                                                      ProxyInfo proxyInfo )
+    {
+        String msg = "Transfer failed for " + url;
+        if ( statusCode != UNKNOWN_STATUS_CODE )
+        {
+            msg += " " + statusCode;
+            // deliberately a null check instead of empty check so that we avoid having to handle
+            // all conceivable default status code messages
+            if ( reasonPhrase != null )
+            {
+                msg += " " + reasonPhrase;
+            }
+        }
+        if ( proxyInfo != null )
+        {
+            msg += " " + proxyInfo.toString();
+        }
+        return msg;
+    }
+
+    /**
+     * Format a consistent message for HTTP related {@link AuthorizationException}.
+     * <p>
+     * The message will always include the URL and status code provided. If empty, the reason phrase is substituted with
+     * a common reason based on status code. {@link ProxyInfo} is only included in the message if not null.
+     *
+     * @param url          the URL associated with the message
+     * @param statusCode   an HTTP response status code related to auth
+     * @param reasonPhrase an HTTP status line reason phrase
+     * @param proxyInfo    proxy server used during the transfer, may be null if none used
+     * @return a consistent message for a HTTP related {@link AuthorizationException}
+     */
+    public static String formatAuthorizationMessage( String url, int statusCode, String reasonPhrase,
+                                                     ProxyInfo proxyInfo )
+    {
+        switch ( statusCode )
+        {
+            case SC_UNAUTHORIZED: // no credentials or auth was not valid
+                return "Authentication failed for " + url + " " + statusCode
+                        + ( StringUtils.isEmpty( reasonPhrase ) ? " Unauthorized" : " " + reasonPhrase );
+
+            case SC_FORBIDDEN: // forbidden based on permissions usually
+                return "Authorization failed for " + url + " " + statusCode
+                        + ( StringUtils.isEmpty( reasonPhrase ) ? " Forbidden" : " " + reasonPhrase );
+
+            case SC_PROXY_AUTH_REQUIRED:
+                return "HTTP proxy server authentication failed for " + url + " " + statusCode
+                        + ( StringUtils.isEmpty( reasonPhrase ) ? " Proxy Authentication Required"
+                        : " " + reasonPhrase );
+            default:
+                break;
+        }
+        String msg = "Authorization failed for " + url;
+        if ( statusCode != UNKNOWN_STATUS_CODE )
+        {
+            msg += " " + statusCode;
+            if ( StringUtils.isNotEmpty( reasonPhrase ) )
+            {
+                msg += " " + reasonPhrase;
+            }
+        }
+        if ( proxyInfo != null )
+        {
+            msg += " " + proxyInfo.toString();
+        }
+        return msg;
+
+    }
+
+    /**
+     * Format a consistent message for HTTP related {@link ResourceDoesNotExistException}.
+     * <p>
+     * The message will always include the URL and status code provided. If empty, the reason phrase is substituted with
+     * the commonly used reason phrases per status code. {@link ProxyInfo} is only included if not null.
+     *
+     * @param url          the URL associated with the message
+     * @param statusCode   an HTTP response status code related to resources not being found
+     * @param reasonPhrase an HTTP status line reason phrase
+     * @param proxyInfo    proxy server used during the transfer, may be null if none used
+     * @return a consistent message for a HTTP related {@link ResourceDoesNotExistException}
+     */
+    public static String formatResourceDoesNotExistMessage( String url, int statusCode, String reasonPhrase,
+                                                            ProxyInfo proxyInfo )
+    {
+        String msg = "Resource missing at " + url;
+
+        if ( statusCode != UNKNOWN_STATUS_CODE )
+        {
+            msg += " " + statusCode;
+
+            if ( StringUtils.isNotEmpty( reasonPhrase ) )
+            {
+                msg += " " + reasonPhrase;
+            }
+            else
+            {
+                if ( statusCode == SC_NOT_FOUND )
+                {
+                    msg += " Not Found";
+                }
+                else if ( statusCode == SC_GONE )
+                {
+                    msg += " Gone";
+                }
+            }
+        }
+        if ( proxyInfo != null )
+        {
+            msg += " " + proxyInfo.toString();
+        }
+        return msg;
+    }
+
+}
diff --git a/wagon-providers/wagon-http/src/main/java/org/apache/maven/wagon/providers/http/HttpWagon.java b/wagon-providers/wagon-http/src/main/java/org/apache/maven/wagon/providers/http/HttpWagon.java
index 6d2f5a1..9d53729 100755
--- a/wagon-providers/wagon-http/src/main/java/org/apache/maven/wagon/providers/http/HttpWagon.java
+++ b/wagon-providers/wagon-http/src/main/java/org/apache/maven/wagon/providers/http/HttpWagon.java
@@ -29,11 +29,16 @@
 import org.apache.maven.wagon.authorization.AuthorizationException;
 import org.apache.maven.wagon.shared.http.AbstractHttpClientWagon;
 import org.apache.maven.wagon.shared.http.HtmlFileListParser;
+import org.apache.maven.wagon.shared.http.HttpMessageUtils;
 
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 
+import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage;
+import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferDebugMessage;
+import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferFailedMessage;
+
 /**
  * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
  */
@@ -64,9 +69,10 @@
             CloseableHttpResponse response = execute( getMethod );
             try
             {
+                String reasonPhrase = response.getStatusLine().getReasonPhrase();
                 int statusCode = response.getStatusLine().getStatusCode();
 
-                fireTransferDebug( url + " - Status code: " + statusCode );
+                fireTransferDebug( formatTransferDebugMessage( url, statusCode, reasonPhrase, getProxyInfo() ) );
 
                 switch ( statusCode )
                 {
@@ -74,24 +80,22 @@
                         break;
 
                     case HttpStatus.SC_FORBIDDEN:
-                        throw new AuthorizationException( "Access denied to: " + url );
-
                     case HttpStatus.SC_UNAUTHORIZED:
-                        throw new AuthorizationException( "Not authorized." );
-
                     case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
-                        throw new AuthorizationException( "Not authorized by proxy." );
+                        throw new AuthorizationException( HttpMessageUtils.formatAuthorizationMessage( url, statusCode,
+                                reasonPhrase, getProxyInfo() ) );
 
                     case HttpStatus.SC_NOT_FOUND:
-                        throw new ResourceDoesNotExistException( "File: " + url + " does not exist" );
+                        throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( url, statusCode,
+                                reasonPhrase, getProxyInfo() ) );
 
                     case SC_TOO_MANY_REQUESTS:
                         return getFileList( backoff( wait, url ), destinationDirectory );
 
                     //add more entries here
                     default:
-                        throw new TransferFailedException(
-                            "Failed to transfer file: " + url + ". Return code is: " + statusCode );
+                        throw new TransferFailedException( formatTransferFailedMessage( url, statusCode, reasonPhrase,
+                                getProxyInfo() ) );
                 }
                 HttpEntity entity = response.getEntity();
                 if ( entity != null )
diff --git a/wagon-providers/wagon-http/src/test/java/org/apache/maven/wagon/providers/http/ErrorWithMessageServlet.java b/wagon-providers/wagon-http/src/test/java/org/apache/maven/wagon/providers/http/ErrorWithMessageServlet.java
index 95a86c1..bf74118 100644
--- a/wagon-providers/wagon-http/src/test/java/org/apache/maven/wagon/providers/http/ErrorWithMessageServlet.java
+++ b/wagon-providers/wagon-http/src/test/java/org/apache/maven/wagon/providers/http/ErrorWithMessageServlet.java
@@ -48,6 +48,10 @@
         {
             response.sendError( 403, MESSAGE );
         }
+        else if ( request.getRequestURL().toString().contains( "404" ) )
+        {
+            response.sendError( 404, MESSAGE );
+        }
         else if ( request.getRequestURL().toString().contains( "407" ) )
         {
             response.sendError( 407, MESSAGE );
diff --git a/wagon-providers/wagon-http/src/test/java/org/apache/maven/wagon/providers/http/HttpWagonErrorTest.java b/wagon-providers/wagon-http/src/test/java/org/apache/maven/wagon/providers/http/HttpWagonErrorTest.java
index 85df40d..83d4387 100644
--- a/wagon-providers/wagon-http/src/test/java/org/apache/maven/wagon/providers/http/HttpWagonErrorTest.java
+++ b/wagon-providers/wagon-http/src/test/java/org/apache/maven/wagon/providers/http/HttpWagonErrorTest.java
@@ -20,6 +20,7 @@
  */
 
 import org.apache.maven.wagon.FileTestUtils;
+import org.apache.maven.wagon.ResourceDoesNotExistException;
 import org.apache.maven.wagon.TransferFailedException;
 import org.apache.maven.wagon.Wagon;
 import org.apache.maven.wagon.authorization.AuthorizationException;
@@ -34,6 +35,8 @@
 public class HttpWagonErrorTest
     extends HttpWagonHttpServerTestCase
 {
+    private int serverPort;
+
     protected void setUp()
         throws Exception
     {
@@ -41,9 +44,10 @@
         ServletHolder servlets = new ServletHolder( new ErrorWithMessageServlet() );
         context.addServlet( servlets, "/*" );
         startServer();
+        serverPort = getPort();
     }
 
-    public void testGetReasonPhase401()
+    public void testGet401()
         throws Exception
     {
         Exception thrown = null;
@@ -53,14 +57,14 @@
             Wagon wagon = getWagon();
 
             Repository testRepository = new Repository();
-            testRepository.setUrl( "http://localhost:" + getPort() );
+            testRepository.setUrl( "http://localhost:" + serverPort );
 
             wagon.connect( testRepository );
 
             File destFile = FileTestUtils.createUniqueFile( getName(), getName() );
             destFile.deleteOnExit();
 
-            wagon.get( "/401", destFile );
+            wagon.get( "401", destFile );
 
             wagon.disconnect();
         }
@@ -75,9 +79,11 @@
 
         assertNotNull( thrown );
         assertEquals( AuthorizationException.class, thrown.getClass() );
+        assertEquals("Authentication failed for http://localhost:" + serverPort
+                + "/401 401 " + ErrorWithMessageServlet.MESSAGE, thrown.getMessage());
     }
 
-    public void testGetReasonPhase403()
+    public void testGet403()
         throws Exception
     {
         Exception thrown = null;
@@ -87,14 +93,14 @@
             Wagon wagon = getWagon();
 
             Repository testRepository = new Repository();
-            testRepository.setUrl( "http://localhost:" + getPort() );
+            testRepository.setUrl( "http://localhost:" + serverPort );
 
             wagon.connect( testRepository );
 
             File destFile = FileTestUtils.createUniqueFile( getName(), getName() );
             destFile.deleteOnExit();
 
-            wagon.get( "/403", destFile );
+            wagon.get( "403", destFile );
 
             wagon.disconnect();
         }
@@ -109,10 +115,47 @@
 
         assertNotNull( thrown );
         assertEquals( AuthorizationException.class, thrown.getClass() );
+        assertEquals("Authorization failed for http://localhost:" + serverPort
+                + "/403 403 " + ErrorWithMessageServlet.MESSAGE, thrown.getMessage());
     }
 
+    public void testGet404()
+            throws Exception
+    {
+        Exception thrown = null;
 
-    public void testGetReasonPhase407()
+        try
+        {
+            Wagon wagon = getWagon();
+
+            Repository testRepository = new Repository();
+            testRepository.setUrl( "http://localhost:" + serverPort );
+
+            wagon.connect( testRepository );
+
+            File destFile = FileTestUtils.createUniqueFile( getName(), getName() );
+            destFile.deleteOnExit();
+
+            wagon.get( "404", destFile );
+
+            wagon.disconnect();
+        }
+        catch ( Exception e )
+        {
+            thrown = e;
+        }
+        finally
+        {
+            stopServer();
+        }
+
+        assertNotNull( thrown );
+        assertEquals( ResourceDoesNotExistException.class, thrown.getClass() );
+        assertEquals("Resource missing at http://localhost:" + serverPort + "/404 404 "
+                + ErrorWithMessageServlet.MESSAGE, thrown.getMessage());
+    }
+
+    public void testGet407()
         throws Exception
     {
         Exception thrown = null;
@@ -129,7 +172,7 @@
             File destFile = FileTestUtils.createUniqueFile( getName(), getName() );
             destFile.deleteOnExit();
 
-            wagon.get( "/407", destFile );
+            wagon.get( "407", destFile );
 
             wagon.disconnect();
         }
@@ -144,9 +187,11 @@
 
         assertNotNull( thrown );
         assertEquals( AuthorizationException.class, thrown.getClass() );
+        assertEquals("HTTP proxy server authentication failed for http://localhost:" + serverPort
+                + "/407 407 " + ErrorWithMessageServlet.MESSAGE, thrown.getMessage());
     }
 
-    public void testGetReasonPhase500()
+    public void testGet500()
         throws Exception
     {
         Exception thrown = null;
@@ -156,14 +201,14 @@
             Wagon wagon = getWagon();
 
             Repository testRepository = new Repository();
-            testRepository.setUrl( "http://localhost:" + getPort() );
+            testRepository.setUrl( "http://localhost:" + serverPort );
 
             wagon.connect( testRepository );
 
             File destFile = FileTestUtils.createUniqueFile( getName(), getName() );
             destFile.deleteOnExit();
 
-            wagon.get( "/500", destFile );
+            wagon.get( "500", destFile );
 
             wagon.disconnect();
         }
@@ -178,5 +223,7 @@
 
         assertNotNull( thrown );
         assertEquals( TransferFailedException.class, thrown.getClass() );
+        assertEquals("Transfer failed for http://localhost:" + serverPort + "/500 500 "
+                + ErrorWithMessageServlet.MESSAGE, thrown.getMessage());
     }
 }
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/WebDavWagonTest.java b/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/WebDavWagonTest.java
index c8a3692..5c07d87 100644
--- a/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/WebDavWagonTest.java
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/WebDavWagonTest.java
@@ -523,4 +523,11 @@
 
     }
 
+    @Override
+    protected void verifyWagonExceptionMessage( Exception e, int forStatusCode, String forUrl, String forReasonPhrase )
+    {
+        Repository repo = new Repository( "test-geturl", forUrl );
+        String expectedMessageUrl = ( new WebDavWagon() ).getURL( repo );
+        super.verifyWagonExceptionMessage( e, forStatusCode, expectedMessageUrl, forReasonPhrase );
+    }
 }
diff --git a/wagon-tcks/wagon-tck-http/src/main/java/org/apache/maven/wagon/tck/http/Assertions.java b/wagon-tcks/wagon-tck-http/src/main/java/org/apache/maven/wagon/tck/http/Assertions.java
index 9a10944..eb7cfa8 100644
--- a/wagon-tcks/wagon-tck-http/src/main/java/org/apache/maven/wagon/tck/http/Assertions.java
+++ b/wagon-tcks/wagon-tck-http/src/main/java/org/apache/maven/wagon/tck/http/Assertions.java
@@ -19,21 +19,36 @@
  * under the License.
  */
 
-import static junit.framework.Assert.assertEquals;
-import static org.codehaus.plexus.util.FileUtils.fileRead;
-
+import org.apache.maven.wagon.ResourceDoesNotExistException;
+import org.apache.maven.wagon.TransferFailedException;
+import org.apache.maven.wagon.WagonException;
+import org.apache.maven.wagon.authorization.AuthorizationException;
+import org.apache.maven.wagon.proxy.ProxyInfo;
 import org.codehaus.plexus.util.IOUtil;
+import org.codehaus.plexus.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import javax.servlet.http.HttpServletResponse;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 
+import static org.codehaus.plexus.util.FileUtils.fileRead;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
 /**
  * 
  */
 public final class Assertions
 {
 
+    public static final int NO_RESPONSE_STATUS_CODE = -1;
+
+    protected static final Logger LOGGER = LoggerFactory.getLogger( Assertions.class );
+
     public static void assertFileContentsFromResource( final String resourceBase, final String resourceName,
                                                        final File output, final String whyWouldItFail )
         throws IOException
@@ -67,4 +82,120 @@
         return resource;
     }
 
+    /**
+     * Assert a WagonException message contains required format and context based on the status code we expected to
+     * trigger it in the first place.
+     * <p>
+     * This implementation represents the most desired assertions, but HttpWagonTestCase sub-classes could override
+     * this method if a specific wagon representation makes it impossible to meet these assertions.
+     *
+     * @param e               an instance of {@link WagonException}
+     * @param forStatusCode   the response status code that triggered the exception
+     * @param forUrl          the url that triggered the exception
+     * @param forReasonPhrase the optional status line reason phrase the server returned
+     */
+    public static void assertWagonExceptionMessage( Exception e, int forStatusCode, String forUrl,
+                                                    String forReasonPhrase, ProxyInfo proxyInfo )
+    {
+        // TODO: handle AuthenticationException for Wagon.connect() calls
+        assertNotNull( e );
+        try
+        {
+            assertTrue( "only verify instances of WagonException", e instanceof WagonException );
+
+            String reasonPhrase;
+            String assertMessageForBadMessage = "exception message not described properly";
+
+            if ( proxyInfo != null )
+            {
+                assertTrue( "message should end with proxy information if proxy was used",
+                        e.getMessage().endsWith( proxyInfo.toString() ) );
+            }
+
+            switch ( forStatusCode )
+            {
+                case HttpServletResponse.SC_NOT_FOUND:
+                    // TODO: add test for 410: Gone?
+                    assertTrue( "404 not found response should throw ResourceDoesNotExistException",
+                            e instanceof ResourceDoesNotExistException );
+                    reasonPhrase = StringUtils.isEmpty( forReasonPhrase ) ? " Not Found" : ( " " + forReasonPhrase );
+                    assertEquals( assertMessageForBadMessage, "Resource missing at " + forUrl + " 404"
+                            + reasonPhrase, e.getMessage() );
+                    break;
+
+                case HttpServletResponse.SC_UNAUTHORIZED:
+                    // FIXME assumes Wagon.get()/put() returning 401 instead of Wagon.connect()
+                    assertTrue( "401 Unauthorized should throw AuthorizationException since "
+                                    + " AuthenticationException is not explicitly declared as thrown from wagon "
+                                    + "methods",
+                            e instanceof AuthorizationException );
+                    reasonPhrase = StringUtils.isEmpty( forReasonPhrase ) ? " Unauthorized" : ( " " + forReasonPhrase );
+                    assertEquals( assertMessageForBadMessage, "Authentication failed for " + forUrl + " 401"
+                            + reasonPhrase, e.getMessage() );
+                    break;
+
+                case HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED:
+                    assertTrue( "407 Proxy authentication required should throw AuthorizationException",
+                            e instanceof AuthorizationException );
+                    reasonPhrase = StringUtils.isEmpty( forReasonPhrase ) ? " Proxy Authentication Required"
+                            : ( " " + forReasonPhrase );
+                    assertEquals( assertMessageForBadMessage, "HTTP proxy server authentication failed for "
+                            + forUrl + " 407" + reasonPhrase, e.getMessage() );
+                    break;
+
+                case HttpServletResponse.SC_FORBIDDEN:
+                    assertTrue( "403 Forbidden should throw AuthorizationException",
+                            e instanceof AuthorizationException );
+                    reasonPhrase = StringUtils.isEmpty( forReasonPhrase ) ? " Forbidden" : ( " " + forReasonPhrase );
+                    assertEquals( assertMessageForBadMessage, "Authorization failed for " + forUrl + " 403"
+                            + reasonPhrase, e.getMessage() );
+                    break;
+
+                default:
+                    assertTrue( "transfer failures should at least be wrapped in a TransferFailedException", e
+                            instanceof TransferFailedException );
+
+                    // the status code and reason phrase cannot always be learned due to implementation limitations
+                    // so the message may not include them, but the implementation should use a consistent format
+                    assertTrue( "message should always include url tried: " + e.getMessage(),
+                            e.getMessage().startsWith( "Transfer failed for " + forUrl ) );
+
+                    String statusCodeStr = forStatusCode == NO_RESPONSE_STATUS_CODE ? ""
+                            : String.valueOf( forStatusCode );
+                    if ( forStatusCode != NO_RESPONSE_STATUS_CODE )
+                    {
+
+                        assertTrue( "if there was a response status line, the status code should be >= 400",
+                                forStatusCode >= HttpServletResponse.SC_BAD_REQUEST );
+
+                        if ( e.getMessage().length() > ( "Transfer failed for " + forUrl ).length() )
+                        {
+                            assertTrue( "message should include url and status code: " + e.getMessage(),
+                                    e.getMessage().startsWith( "Transfer failed for " + forUrl + statusCodeStr ) );
+                        }
+
+                        reasonPhrase = forReasonPhrase == null ? "" : " " + forReasonPhrase;
+
+                        if ( reasonPhrase.length() > 0 && e.getMessage().length() > ( "Transfer failed for " + forUrl
+                                + statusCodeStr ).length() )
+                        {
+                            assertTrue( "message should include url and status code and reason phrase: "
+                                    + e.getMessage(), e.getMessage().startsWith( "Transfer failed for "
+                                            + forUrl + statusCodeStr
+                                            + reasonPhrase ) );
+                        }
+
+                    }
+
+                    break;
+            }
+        }
+        catch ( AssertionError assertionError )
+        {
+            LOGGER.error( "Exception which failed assertions: ", e );
+            throw assertionError;
+        }
+
+    }
+
 }
diff --git a/wagon-tcks/wagon-tck-http/src/main/java/org/apache/maven/wagon/tck/http/GetWagonTests.java b/wagon-tcks/wagon-tck-http/src/main/java/org/apache/maven/wagon/tck/http/GetWagonTests.java
index 1562369..ac48636 100644
--- a/wagon-tcks/wagon-tck-http/src/main/java/org/apache/maven/wagon/tck/http/GetWagonTests.java
+++ b/wagon-tcks/wagon-tck-http/src/main/java/org/apache/maven/wagon/tck/http/GetWagonTests.java
@@ -45,7 +45,9 @@
 
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.fail;
+import static org.apache.maven.wagon.tck.http.Assertions.NO_RESPONSE_STATUS_CODE;
 import static org.apache.maven.wagon.tck.http.Assertions.assertFileContentsFromResource;
+import static org.apache.maven.wagon.tck.http.Assertions.assertWagonExceptionMessage;
 
 /**
  * 
@@ -114,7 +116,7 @@
             return;
         }
 
-        final ValueHolder<Boolean> holder = new ValueHolder<Boolean>( false );
+        final ValueHolder<TransferFailedException> holder = new ValueHolder<>( null );
 
         Runnable r = new Runnable()
         {
@@ -141,34 +143,15 @@
 
                     fail( "Should have failed to transfer due to transaction timeout." );
                 }
-                catch ( ConnectionException e )
-                {
-                    throw new IllegalStateException( e );
-                }
-                catch ( AuthenticationException e )
+                catch ( ConnectionException | AuthenticationException | ResourceDoesNotExistException
+                        | AuthorizationException | ComponentConfigurationException | IOException e )
                 {
                     throw new IllegalStateException( e );
                 }
                 catch ( TransferFailedException e )
                 {
                     // expected
-                    holder.setValue( true );
-                }
-                catch ( ResourceDoesNotExistException e )
-                {
-                    throw new IllegalStateException( e );
-                }
-                catch ( AuthorizationException e )
-                {
-                    throw new IllegalStateException( e );
-                }
-                catch ( ComponentConfigurationException e )
-                {
-                    throw new IllegalStateException( e );
-                }
-                catch ( IOException e )
-                {
-                    throw new IllegalStateException( e );
+                    holder.setValue( e );
                 }
             }
         };
@@ -189,7 +172,9 @@
         logger.info( "Interrupting thread." );
         t.interrupt();
 
-        assertTrue( "TransferFailedException should have been thrown.", holder.getValue() );
+        assertTrue( "TransferFailedException should have been thrown.", holder.getValue() != null );
+        assertWagonExceptionMessage( holder.getValue(), NO_RESPONSE_STATUS_CODE, getBaseUrl() + "infinite/",
+                "", null );
     }
 
     @Test
@@ -214,6 +199,8 @@
         catch ( TransferFailedException e )
         {
             // expected
+            assertWagonExceptionMessage( e, NO_RESPONSE_STATUS_CODE,  "http://localhost:65520/base.txt",
+                    null, null );
         }
     }