Repository: mapproxy/mapproxy
Issue: failing GFI requests with special characters (umlauts) when lxml is installed

<!--- It is a bug! -->
<!--- Please provide a general summary of the issue in the Title above -->

We have started to use XLST scripts to transform GetFeatureInfo responses of a source WMS. Generally, this works fine and as expected. However, some of the XML responses contain attributes with special characters (in our case: German umlauts) which lead to misformed XML responses. This behavior also shows when the lxml package is installed but no actual XSLT scripts are enabled.

It seems that the proxying of GFI responses differs depending on the availability of the lxml package.

## Context

The source WMS GetFeatureInfo XML is:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<FeatureInfoResponse xmlns:esri_wms="http://www.esri.com/wms" xmlns="http://www.esri.com/wms">
    <FIELDS OBJECTID="21" AttributeWithÄ="ö" />
</FeatureInfoResponse>
```

✅ MapProxy's proxied XML *without* lxml installed is:
```xml
<?xml version="1.0" encoding="UTF-8"?>

<FeatureInfoResponse xmlns:esri_wms="http://www.esri.com/wms" xmlns="http://www.esri.com/wms">
    <FIELDS OBJECTID="21" AttributeWithÄ="ö" />
</FeatureInfoResponse>
```


❌ MapProxy's proxied XML *with* lxml installed is:
```xml
<FeatureInfoResponse xmlns:esri_wms="http://www.esri.com/wms" xmlns="http://www.esri.com/wms">
    <FIELDS OBJECTID="21" AttributeWith&#228;="&#246;" />
</FeatureInfoResponse>
```

> Note the broken attribute at `&#228;` - also, the XML version tag is omitted.

## Expected Behavior

I would expect both proxied GetFeatureInfo responses to be identical when no XSLT transformations are configured.

## Actual Behavior

When the lxml package is installed, special characters are being transformed into their unicode codes which results in malformed XML documents (when the special characters occur, e.g. as attribute name as in the example above)

## Your Environment

I use the stable [`kartoza/docker-mapproxy`](https://github.com/kartoza/docker-mapproxy) base image with MapProxy 1.14.0. 
lxml is installed in the Dockerfile via `pip install lxml`. 

Changed files:
mapproxy/test/unit/test_featureinfo.py, mapproxy/featureinfo.py

diff --git a/mapproxy/featureinfo.py b/mapproxy/featureinfo.py
index 7d9b2880..37e01001 100644
--- a/mapproxy/featureinfo.py
+++ b/mapproxy/featureinfo.py
@@ -58,6 +58,7 @@ class TextFeatureInfoDoc(FeatureInfoDoc):
 
 class XMLFeatureInfoDoc(FeatureInfoDoc):
     info_type = "xml"
+    defaultEncoding = "UTF-8"
 
     def __init__(self, content):
         if isinstance(content, (string_type, bytes)):
@@ -81,7 +82,9 @@ class XMLFeatureInfoDoc(FeatureInfoDoc):
         return self._etree
 
     def _serialize_etree(self):
-        return etree.tostring(self._etree)
+        encoding = self._etree.docinfo.encoding if \\
+            self._etree.docinfo.encoding else self.defaultEncoding
+        return etree.tostring(self._etree, encoding=encoding, xml_declaration=False)
 
     def _parse_content(self):
         doc = as_io(self._str_content)
diff --git a/mapproxy/test/unit/test_featureinfo.py b/mapproxy/test/unit/test_featureinfo.py
index 53a1ac28..a127be5e 100644
--- a/mapproxy/test/unit/test_featureinfo.py
+++ b/mapproxy/test/unit/test_featureinfo.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 # This file is part of the MapProxy project.
 # Copyright (C) 2011 Omniscale <http://omniscale.de>
 #
@@ -14,7 +15,9 @@
 # limitations under the License.
 
 import os
+import sys
 import tempfile
+import pytest
 
 from lxml import etree, html
 
@@ -95,6 +98,15 @@ class TestXMLFeatureInfoDocs(object):
         doc = XMLFeatureInfoDoc("<root>hello</root>")
         assert doc.as_etree().getroot().text == "hello"
 
+    @pytest.mark.skipif(sys.version_info < (3, 0), reason="test skipped for python 2")
+    def test_umlauts(self):
+        doc = XMLFeatureInfoDoc('<root>öäüß</root>')
+        assert doc.as_etree().getroot().text == 'öäüß'
+
+        input_tree = etree.fromstring('<root>öäüß</root>'.encode('utf-8'))
+        doc = XMLFeatureInfoDoc(input_tree)
+        assert doc.as_string().decode("utf-8") == '<root>öäüß</root>'
+
     def test_combine(self):
         docs = [
             XMLFeatureInfoDoc("<root><a>foo</a></root>"),