Index: /trunk/src/org/openstreetmap/josm/data/imagery/GeorefImage.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/imagery/GeorefImage.java	(revision 7424)
+++ /trunk/src/org/openstreetmap/josm/data/imagery/GeorefImage.java	(revision 7425)
@@ -73,8 +73,8 @@
             image.flush();
         }
-        changeImage(null, null);
-    }
-
-    public void changeImage(State state, BufferedImage image) {
+        changeImage(null, null, null);
+    }
+
+    public void changeImage(State state, BufferedImage image, String errorMsg) {
         flushResizedCachedInstance();
         this.image = image;
@@ -85,5 +85,5 @@
         case FAILED:
             BufferedImage imgFailed = createImage();
-            layer.drawErrorTile(imgFailed);
+            layer.drawErrorTile(imgFailed, errorMsg);
             this.image = imgFailed;
             break;
Index: /trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java	(revision 7424)
+++ /trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java	(revision 7425)
@@ -9,12 +9,20 @@
 import java.awt.Component;
 import java.awt.Font;
-import java.awt.Graphics;
+import java.awt.Graphics2D;
 import java.awt.GridBagLayout;
 import java.awt.event.ActionEvent;
+import java.awt.font.FontRenderContext;
+import java.awt.font.LineBreakMeasurer;
+import java.awt.font.TextAttribute;
+import java.awt.font.TextLayout;
 import java.awt.image.BufferedImage;
 import java.awt.image.BufferedImageOp;
 import java.awt.image.ConvolveOp;
 import java.awt.image.Kernel;
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.util.Hashtable;
 import java.util.List;
+import java.util.Map;
 
 import javax.swing.AbstractAction;
@@ -68,4 +76,7 @@
     private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this);
 
+    /**
+     * Constructs a new {@code ImageryLayer}.
+     */
     public ImageryLayer(ImageryInfo info) {
         super(info.getName());
@@ -81,5 +92,5 @@
     }
 
-    public double getPPD(){
+    public double getPPD() {
         if (!Main.isDisplayingMapView()) return Main.getProjection().getDefaultZoomInPPD();
         ProjectionBounds bounds = Main.map.mapView.getProjectionBounds();
@@ -231,18 +242,60 @@
     }
 
-    public void drawErrorTile(BufferedImage img) {
-        Graphics g = img.getGraphics();
+    /**
+     * Draws a red error tile when imagery tile cannot be fetched.
+     * @param img The buffered image
+     * @param message Additional error message to display
+     */
+    public void drawErrorTile(BufferedImage img, String message) {
+        Graphics2D g = (Graphics2D) img.getGraphics();
         g.setColor(Color.RED);
         g.fillRect(0, 0, img.getWidth(), img.getHeight());
-        g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(36.0f));
+        g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(24.0f));
         g.setColor(Color.BLACK);
 
         String text = tr("ERROR");
-        g.drawString(text, (img.getWidth() + g.getFontMetrics().stringWidth(text)) / 2, img.getHeight()/2);
-    }
-
-    /* (non-Javadoc)
-     * @see org.openstreetmap.josm.gui.layer.Layer#destroy()
-     */
+        g.drawString(text, (img.getWidth() - g.getFontMetrics().stringWidth(text)) / 2, g.getFontMetrics().getHeight()+5);
+        if (message != null) {
+            float drawPosY = 2.5f*g.getFontMetrics().getHeight()+10;
+            if (!message.contains(" ")) {
+                g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(18.0f));
+                g.drawString(message, 5, (int)drawPosY);
+            } else {
+                // Draw message on several lines
+                Map<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
+                map.put(TextAttribute.FAMILY, "Serif");
+                map.put(TextAttribute.SIZE, new Float(18.0));
+                AttributedString vanGogh = new AttributedString(message, map);
+                // Create a new LineBreakMeasurer from the text
+                AttributedCharacterIterator paragraph = vanGogh.getIterator();
+                int paragraphStart = paragraph.getBeginIndex();
+                int paragraphEnd = paragraph.getEndIndex();
+                FontRenderContext frc = g.getFontRenderContext();
+                LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
+                // Set break width to width of image with some margin
+                float breakWidth = img.getWidth()-10;
+                // Set position to the index of the first character in the text
+                lineMeasurer.setPosition(paragraphStart);
+                // Get lines until the entire paragraph has been displayed
+                while (lineMeasurer.getPosition() < paragraphEnd) {
+                    // Retrieve next layout
+                    TextLayout layout = lineMeasurer.nextLayout(breakWidth);
+
+                    // Compute pen x position
+                    float drawPosX = layout.isLeftToRight() ? 0 : breakWidth - layout.getAdvance();
+
+                    // Move y-coordinate by the ascent of the layout
+                    drawPosY += layout.getAscent();
+
+                    // Draw the TextLayout at (drawPosX, drawPosY)
+                    layout.draw(g, drawPosX, drawPosY);
+
+                    // Move y-coordinate in preparation for next layout
+                    drawPosY += layout.getDescent() + layout.getLeading();
+                }
+            }
+        }
+    }
+
     @Override
     public void destroy() {
Index: /trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java	(revision 7424)
+++ /trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java	(revision 7425)
@@ -62,6 +62,6 @@
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
 import org.openstreetmap.josm.io.WMSLayerImporter;
-import org.openstreetmap.josm.io.imagery.Grabber;
 import org.openstreetmap.josm.io.imagery.HTMLGrabber;
+import org.openstreetmap.josm.io.imagery.WMSException;
 import org.openstreetmap.josm.io.imagery.WMSGrabber;
 import org.openstreetmap.josm.io.imagery.WMSRequest;
@@ -147,5 +147,5 @@
     private final Lock requestQueueLock = new ReentrantLock();
     private final Condition queueEmpty = requestQueueLock.newCondition();
-    private final List<Grabber> grabbers = new ArrayList<>();
+    private final List<WMSGrabber> grabbers = new ArrayList<>();
     private final List<Thread> grabberThreads = new ArrayList<>();
     private boolean canceled;
@@ -163,4 +163,7 @@
     }
 
+    /**
+     * Constructs a new {@code WMSLayer}.
+     */
     public WMSLayer(ImageryInfo info) {
         super(info);
@@ -648,5 +651,6 @@
                 GeorefImage img = images[modulo(request.getXIndex(),dax)][modulo(request.getYIndex(),day)];
                 if (img.equalPosition(request.getXIndex(), request.getYIndex())) {
-                    img.changeImage(request.getState(), request.getImage());
+                    WMSException we = request.getException();
+                    img.changeImage(request.getState(), request.getImage(), we != null ? we.getMessage() : null);
                 }
             }
@@ -931,5 +935,4 @@
             Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree());
         }
-
     }
 
@@ -938,5 +941,5 @@
         try {
             canceled = true;
-            for (Grabber grabber: grabbers) {
+            for (WMSGrabber grabber: grabbers) {
                 grabber.cancel();
             }
@@ -964,5 +967,5 @@
             grabberThreads.clear();
             for (int i=0; i<threadCount; i++) {
-                Grabber grabber = getGrabber(i == 0 && threadCount > 1);
+                WMSGrabber grabber = getGrabber(i == 0 && threadCount > 1);
                 grabbers.add(grabber);
                 Thread t = new Thread(grabber, "WMS " + getName() + " " + i);
@@ -1005,5 +1008,5 @@
     }
 
-    protected Grabber getGrabber(boolean localOnly) {
+    protected WMSGrabber getGrabber(boolean localOnly) {
         if (getInfo().getImageryType() == ImageryType.HTML)
             return new HTMLGrabber(Main.map.mapView, this, localOnly);
Index: unk/src/org/openstreetmap/josm/io/imagery/Grabber.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/imagery/Grabber.java	(revision 7424)
+++ 	(revision )
@@ -1,91 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.io.imagery;
-
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.data.ProjectionBounds;
-import org.openstreetmap.josm.data.imagery.GeorefImage.State;
-import org.openstreetmap.josm.gui.MapView;
-import org.openstreetmap.josm.gui.layer.WMSLayer;
-
-public abstract class Grabber implements Runnable {
-    protected final MapView mv;
-    protected final WMSLayer layer;
-    private final boolean localOnly;
-
-    protected ProjectionBounds b;
-    protected volatile boolean canceled;
-
-    Grabber(MapView mv, WMSLayer layer, boolean localOnly) {
-        this.mv = mv;
-        this.layer = layer;
-        this.localOnly = localOnly;
-    }
-
-    abstract void fetch(WMSRequest request, int attempt) throws Exception; // the image fetch code
-
-    int width(){
-        return layer.getBaseImageWidth();
-    }
-
-    int height(){
-        return layer.getBaseImageHeight();
-    }
-
-    @Override
-    public void run() {
-        while (true) {
-            if (canceled)
-                return;
-            WMSRequest request = layer.getRequest(localOnly);
-            if (request == null)
-                return;
-            this.b = layer.getBounds(request);
-            if (request.isPrecacheOnly()) {
-                if (!layer.cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth)) {
-                    attempt(request);
-                } else if (Main.isDebugEnabled()) {
-                    Main.debug("Ignoring "+request+" (precache only + exact match)");
-                }
-            } else if (!loadFromCache(request)){
-                attempt(request);
-            } else if (Main.isDebugEnabled()) {
-                Main.debug("Ignoring "+request+" (loaded from cache)");
-            }
-            layer.finishRequest(request);
-        }
-    }
-
-    protected void attempt(WMSRequest request){ // try to fetch the image
-        int maxTries = 5; // n tries for every image
-        for (int i = 1; i <= maxTries; i++) {
-            if (canceled)
-                return;
-            try {
-                if (!request.isPrecacheOnly() && !layer.requestIsVisible(request))
-                    return;
-                fetch(request, i);
-                break; // break out of the retry loop
-            } catch (Exception e) {
-                try { // sleep some time and then ask the server again
-                    Thread.sleep(random(1000, 2000));
-                } catch (InterruptedException e1) {
-                    Main.debug("InterruptedException in "+getClass().getSimpleName()+" during WMS request");
-                }
-                if (i == maxTries) {
-                    Main.error(e);
-                    request.finish(State.FAILED, null);
-                }
-            }
-        }
-    }
-
-    public static int random(int min, int max) {
-        return (int)(Math.random() * ((max+1)-min) ) + min;
-    }
-
-    public abstract boolean loadFromCache(WMSRequest request);
-
-    public void cancel() {
-        canceled = true;
-    }
-}
Index: /trunk/src/org/openstreetmap/josm/io/imagery/WMSException.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/imagery/WMSException.java	(revision 7425)
+++ /trunk/src/org/openstreetmap/josm/io/imagery/WMSException.java	(revision 7425)
@@ -0,0 +1,59 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.imagery;
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * WMS Service Exception, as defined by {@code application/vnd.ogc.se_xml} format:<ul>
+ * <li><a href="http://schemas.opengis.net/wms/1.1.0/exception_1_1_0.dtd">WMS 1.1.0 DTD</a></li>
+ * <li><a href="http://schemas.opengis.net/wms/1.3.0/exception_1_3_0.dtd">WMS 1.3.0 XSD</a></li>
+ * </ul>
+ * @since 7425
+ */
+public class WMSException extends Exception {
+
+    private final WMSRequest request;
+    private final URL url;
+    private final String[] exceptions;
+
+    /**
+     * Constructs a new {@code WMSException}.
+     * @param request the WMS request that lead to this exception
+     * @param url the URL that lead to this exception
+     * @param exceptions the exceptions replied by WMS server
+     */
+    public WMSException(WMSRequest request, URL url, Collection<String> exceptions) {
+        super(Utils.join("\n", exceptions));
+        this.request = request;
+        this.url = url;
+        this.exceptions = exceptions.toArray(new String[0]);
+    }
+
+    /**
+     * Replies the WMS request that lead to this exception.
+     * @return the WMS request
+     */
+    public final WMSRequest getRequest() {
+        return request;
+    }
+
+    /**
+     * Replies the URL that lead to this exception.
+     * @return the URL
+     */
+    public final URL getUrl() {
+        return url;
+    }
+
+    /**
+     * Replies the WMS Service exceptions.
+     * @return the exceptions
+     */
+    public final Collection<String> getExceptions() {
+        return Arrays.asList(exceptions);
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java	(revision 7424)
+++ /trunk/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java	(revision 7425)
@@ -9,4 +9,5 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.StringReader;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
@@ -17,5 +18,7 @@
 import java.text.DecimalFormatSymbols;
 import java.text.NumberFormat;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -24,5 +27,10 @@
 import java.util.regex.Pattern;
 
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.ProjectionBounds;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
@@ -35,6 +43,21 @@
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Utils;
-
-public class WMSGrabber extends Grabber {
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * WMS grabber, fetching tiles from WMS server.
+ * @since 3715
+ */
+public class WMSGrabber implements Runnable {
+
+    protected final MapView mv;
+    protected final WMSLayer layer;
+    private final boolean localOnly;
+
+    protected ProjectionBounds b;
+    protected volatile boolean canceled;
 
     protected String baseURL;
@@ -42,9 +65,16 @@
     private Map<String, String> props = new HashMap<>();
 
+    /**
+     * Constructs a new {@code WMSGrabber}.
+     * @param mv Map view
+     * @param layer WMS layer
+     */
     public WMSGrabber(MapView mv, WMSLayer layer, boolean localOnly) {
-        super(mv, layer, localOnly);
+        this.mv = mv;
+        this.layer = layer;
+        this.localOnly = localOnly;
         this.info = layer.getInfo();
         this.baseURL = info.getUrl();
-        if(layer.getInfo().getCookies() != null && !layer.getInfo().getCookies().isEmpty()) {
+        if (layer.getInfo().getCookies() != null && !layer.getInfo().getCookies().isEmpty()) {
             props.put("Cookie", layer.getInfo().getCookies());
         }
@@ -60,6 +90,75 @@
     }
 
+    int width() {
+        return layer.getBaseImageWidth();
+    }
+
+    int height() {
+        return layer.getBaseImageHeight();
+    }
+
     @Override
-    void fetch(WMSRequest request, int attempt) throws Exception{
+    public void run() {
+        while (true) {
+            if (canceled)
+                return;
+            WMSRequest request = layer.getRequest(localOnly);
+            if (request == null)
+                return;
+            this.b = layer.getBounds(request);
+            if (request.isPrecacheOnly()) {
+                if (!layer.cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth)) {
+                    attempt(request);
+                } else if (Main.isDebugEnabled()) {
+                    Main.debug("Ignoring "+request+" (precache only + exact match)");
+                }
+            } else if (!loadFromCache(request)){
+                attempt(request);
+            } else if (Main.isDebugEnabled()) {
+                Main.debug("Ignoring "+request+" (loaded from cache)");
+            }
+            layer.finishRequest(request);
+        }
+    }
+
+    protected void attempt(WMSRequest request){ // try to fetch the image
+        int maxTries = 5; // n tries for every image
+        for (int i = 1; i <= maxTries; i++) {
+            if (canceled)
+                return;
+            try {
+                if (!request.isPrecacheOnly() && !layer.requestIsVisible(request))
+                    return;
+                fetch(request, i);
+                break; // break out of the retry loop
+            } catch (IOException e) {
+                try { // sleep some time and then ask the server again
+                    Thread.sleep(random(1000, 2000));
+                } catch (InterruptedException e1) {
+                    Main.debug("InterruptedException in "+getClass().getSimpleName()+" during WMS request");
+                }
+                if (i == maxTries) {
+                    Main.error(e);
+                    request.finish(State.FAILED, null, null);
+                }
+            } catch (WMSException e) {
+                // Fail fast in case of WMS Service exception: useless to retry:
+                // either the URL is wrong or the server suffers huge problems
+                Main.error("WMS service exception while requesting "+e.getUrl()+":\n"+e.getMessage().trim());
+                request.finish(State.FAILED, null, e);
+                break; // break out of the retry loop
+            }
+        }
+    }
+
+    public static int random(int min, int max) {
+        return (int)(Math.random() * ((max+1)-min) ) + min;
+    }
+
+    public final void cancel() {
+        canceled = true;
+    }
+
+    private void fetch(WMSRequest request, int attempt) throws IOException, WMSException {
         URL url = null;
         try {
@@ -68,14 +167,13 @@
                     b.maxEast, b.maxNorth,
                     width(), height());
-            request.finish(State.IMAGE, grab(request, url, attempt));
-
-        } catch(Exception e) {
+            request.finish(State.IMAGE, grab(request, url, attempt), null);
+
+        } catch (IOException | OsmTransferException e) {
             Main.error(e);
-            throw new Exception(e.getMessage() + "\nImage couldn't be fetched: " + (url != null ? url.toString() : ""), e);
-        }
-    }
-
-    public static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000",
-            new DecimalFormatSymbols(Locale.US));
+            throw new IOException(e.getMessage() + "\nImage couldn't be fetched: " + (url != null ? url.toString() : ""), e);
+        }
+    }
+
+    public static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
 
     protected URL getURL(double w, double s,double e,double n,
@@ -133,5 +231,4 @@
     }
 
-    @Override
     public boolean loadFromCache(WMSRequest request) {
         BufferedImage cached = layer.cache.getExactMatch(
@@ -139,5 +236,5 @@
 
         if (cached != null) {
-            request.finish(State.IMAGE, cached);
+            request.finish(State.IMAGE, cached, null);
             return true;
         } else if (request.isAllowPartialCacheMatch()) {
@@ -145,11 +242,11 @@
                     Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
             if (partialMatch != null) {
-                request.finish(State.PARTLY_IN_CACHE, partialMatch);
+                request.finish(State.PARTLY_IN_CACHE, partialMatch, null);
                 return true;
             }
         }
 
-        if((!request.isReal() && !layer.hasAutoDownload())){
-            request.finish(State.NOT_IN_CACHE, null);
+        if ((!request.isReal() && !layer.hasAutoDownload())){
+            request.finish(State.NOT_IN_CACHE, null, null);
             return true;
         }
@@ -158,9 +255,9 @@
     }
 
-    protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws IOException, OsmTransferException {
+    protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws WMSException, IOException, OsmTransferException {
         Main.info("Grabbing WMS " + (attempt > 1? "(attempt " + attempt + ") ":"") + url);
 
         HttpURLConnection conn = Utils.openHttpConnection(url);
-        for(Entry<String, String> e : props.entrySet()) {
+        for (Entry<String, String> e : props.entrySet()) {
             conn.setRequestProperty(e.getKey(), e.getValue());
         }
@@ -169,7 +266,21 @@
 
         String contentType = conn.getHeaderField("Content-Type");
-        if( conn.getResponseCode() != 200
-                || contentType != null && !contentType.startsWith("image") )
-            throw new IOException(readException(conn));
+        if (conn.getResponseCode() != 200
+                || contentType != null && !contentType.startsWith("image") ) {
+            String xml = readException(conn);
+            try {
+                DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+                InputSource is = new InputSource(new StringReader(xml));
+                Document doc = db.parse(is);
+                NodeList nodes = doc.getElementsByTagName("ServiceException");
+                List<String> exceptions = new ArrayList<>(nodes.getLength());
+                for (int i = 0; i < nodes.getLength(); i++) {
+                    exceptions.add(nodes.item(i).getTextContent());
+                }
+                throw new WMSException(request, url, exceptions);
+            } catch (SAXException | ParserConfigurationException ex) {
+                throw new IOException(xml, ex);
+            }
+        }
 
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
Index: /trunk/src/org/openstreetmap/josm/io/imagery/WMSRequest.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/imagery/WMSRequest.java	(revision 7424)
+++ /trunk/src/org/openstreetmap/josm/io/imagery/WMSRequest.java	(revision 7425)
@@ -19,4 +19,5 @@
     private State state;
     private BufferedImage image;
+    private WMSException exception;
 
     public WMSRequest(int xIndex, int yIndex, double pixelPerDegree, boolean real, boolean allowPartialCacheMatch) {
@@ -33,8 +34,8 @@
     }
 
-
-    public void finish(State state, BufferedImage image) {
+    public void finish(State state, BufferedImage image, WMSException exception) {
         this.state = state;
         this.image = image;
+        this.exception = exception;
     }
 
@@ -97,10 +98,27 @@
     }
 
+    /**
+     * Replies the resulting state.
+     * @return the resulting state
+     */
     public State getState() {
         return state;
     }
 
+    /**
+     * Replies the resulting image, if any.
+     * @return the resulting image, or {@code null}
+     */
     public BufferedImage getImage() {
         return image;
+    }
+
+    /**
+     * Replies the resulting exception, if any.
+     * @return the resulting exception, or {@code null}
+     * @since 7425
+     */
+    public WMSException getException() {
+        return exception;
     }
 
