WCF supported charsets and “Unable to translate bytes [xx] at index 0 from specified code page to Unicode” error

In alcuni scenari di interoperabilità tra client WCF e web services di altri produttori, potreste trovarvi di fronte ad un’eccezione simile a questa quando il servizio viene invocato (estratto dai trace di WCF, source: System.ServiceModel):

 <InnerException>
<ExceptionType>System.Xml.XmlException, System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>
<Message>'�- questa è una prova' contiene byte UTF8 non validi.</Message>
<StackTrace>
in System.Xml.XmlConverter.ToChars(Byte[] buffer, Int32 offset, Int32 count, Char[] chars, Int32 charOffset)
in System.Xml.XmlBufferReader.GetChars(Int32 offset, Int32 length, Char[] chars)
in System.Xml.XmlBufferReader.GetString(Int32 offset, Int32 length)
in System.Xml.ValueHandle.GetCharsText()
in System.Xml.ValueHandle.GetString()
in System.Xml.XmlBaseReader.XmlNode.get_ValueAsString()
in System.Xml.XmlBaseReader.get_Value()
in System.Xml.XmlDictionaryReader.ReadString(Int32 maxStringContentLength)
in System.Xml.XmlDictionaryReader.ReadString()
in System.Xml.Serialization.XmlSerializationReader.ReadStringValue()
[…]
in System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
</StackTrace>
<ExceptionString>System.Xml.XmlException: '� questa è una prova' contiene byte UTF8 non validi. ---&gt; System.Text.DecoderFallbackException: Impossibile convertire i byte [B0] dell'indice 0 dalla tabella codici specificata in Unicode.
     […]
   in System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)</ExceptionString>
<InnerException>
<ExceptionType>System.Text.DecoderFallbackException, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>
<Message>Impossibile convertire i byte [B0] dell'indice 0 dalla tabella codici specificata in Unicode.</Message>
<StackTrace>
in System.Text.DecoderExceptionFallbackBuffer.Throw(Byte[] bytesUnknown, Int32 index)
in System.Text.DecoderExceptionFallbackBuffer.Fallback(Byte[] bytesUnknown, Int32 index)
in System.Text.DecoderFallbackBuffer.InternalFallback(Byte[] bytes, Byte* pBytes, Char*&amp; chars)
in System.Text.UTF8Encoding.FallbackInvalidByteSequence(Byte*&amp; pSrc, Int32 ch, DecoderFallbackBuffer fallback, Char*&amp; pTarget)
in System.Text.UTF8Encoding.GetChars(Byte* bytes, Int32 byteCount, Char* chars, Int32 charCount, DecoderNLS baseDecoder)
in System.Text.UTF8Encoding.GetChars(Byte[] bytes, Int32 byteIndex, Int32 byteCount, Char[] chars, Int32 charIndex)
in System.Xml.XmlConverter.ToChars(Byte[] buffer, Int32 offset, Int32 count, Char[] chars, Int32 charOffset)
</StackTrace>
<ExceptionString>System.Text.DecoderFallbackException: Impossibile convertire i byte [B0] dell'indice 0 dalla tabella codici specificata in Unicode.
 […]   in System.Xml.XmlConverter.ToChars(Byte[] buffer, Int32 offset, Int32 count, Char[] chars, Int32 charOffset)</ExceptionString>
</InnerException>
</InnerException>

Gli errori “Impossibile convertire i byte [B0] dell'indice 0 dalla tabella codici specificata in Unicode. ” e “System.Xml.XmlException: '�- Questa è una prova ' contiene byte UTF8 non validi. ” ci dicono in modo inequivocabile che il client incontra un carattere che non è riconosciuto tra i caratteri validi in UTF-8 mentre sta cercando di deserializzare il messaggio SOAP proveniente dal web service. Naturalmente lo stesso tipo di problema può essere incontrato anche nel caso di un WCF service che viene invocato da in client di terze parti.

Le domande da porsi sono:

1) Perché WCF si aspetta che venga usato l’encoding UTF-8?

2) Che tipo di encoding viene effettivamente utilizzato dal web service di terze parti?

3) Una volta trovate le risposte alle precedenti domande, come è possibile cambiare il comportamento di WCF (supponendo di non voler/poter toccare il web service)?

Cerchiamo ora di rispondere alle domande con ordine:

1) WCF supporta nativamente le seguenti codifiche: UTF-8, UTF-16 and Big Endean Unicode. L’encoding utilizzato by default è UTF8, ma è possibile configurare un encoding diverso cambiando la configurazione del binding utilizzato. Per esempio, il basicHttpBinding ha il parametro “textEncoding” che può essere impostato ad uno dei seguenti valori: “utf-8/utf-16/utf16-LE”.

Se WCF è impostato con UTF-8, si aspetta che la risposta sia codificata con lo stesso standard: se non è così viene lanciata un’eccezione. Vi starete chiedendo come mai allora l’eccezione riportata sopra viene lanciata nel pieno della deserializzazione cercando di decodificare in UTF-8 un messaggio che è codificato diversamente: la risposta è nel punto seguente.

2) Come vi sarà già noto, i messaggi SOAP vengono trasportati come body di una HTTP POST, pertanto vi sono diversi header HTTP a corredo. Ecco un esempio di richiesta, catturata con i System.Net traces:

 System.Net Information: 0 : [2256] HttpWebRequest#27504314 - Request: POST /service HTTP/1.1                                                                                                                                                                                                                                                                                               
System.Net.Sockets Verbose: 0 : [2256] Socket#31421019::Send()                                                                                                                                                  
System.Net.Sockets Verbose: 0 : [2256] Data from Socket#31421019::Send                                                                                                                                          
[...]   
System.Net Information: 0 : [2256] ConnectStream#14353717 - Invio intestazioni                                                                                                                                  
{                                                                                                                                                                                                               
Content-Type: text/xml; charset=utf-8                                                                                                                                                                        
VsDebuggerCausalityData: uIDPo8v1RW/K0TNOmSMk8B42A5YAAAAAyQV8zRhhSku4ya8t3ImjtrLiR3IxDMRNi5ustys8q94ACQAA                                                                                                       
SOAPAction: ""                                                                                                                                                                                                  
Host: hostname.com:8023                                                                                                                                                               
Content-Length: 7760                                                                                                                                                                                            
Expect: 100-continue                                                                                                                                                                                            
Connection: Keep-Alive                                                                                                                                                                                          
}.                              

Come si può notare, è l’header Content-Type che riporta normalmente il formato del body è il charset utilizzato. Il Content-Type dovrebbe essere presente anche nelle SOAP response e il charset dovrebbe semrpe essere chiaramente specificato.

In questo modo sarebbe molto semplice capire qual è l’encoding utizzato, ma purtroppo alcuni web service omettono il charset, rendendo più difficile il compito di WCF. A quel punto il client WCF assumerà che l’encoding utilizzato è UTF-8, con tutti i rischi che ne conseguono.

La risposta al punto 2) quindi è: guardare il charset indicato nell’header Content-Type, se presente; altrimenti è necessario chiedere questa info a chi ha sviluppato/manutiene il web service di terze parti.

3) Come già spiegato nella risposta 1), è molto facile cambiare l’encoding utilizzato da WCF, ammesso che si tratti di uno tra UTF-8, UTF-16, UTF-16LE. Cosa fare se il servizio sta utilizzando un altro encoding? A quel punto le cose si fanno un pochino più complicate, ma non troppo.

Sfruttando uno degli innumerevoli extensibility point di WCF, è possibile sviluppare un custom messageencoder, in modo da impostare una codifica differente: bisogna derivare da MessageEncoder

 public class CustomTextMessageEncoder : MessageEncoder
   {
       private CustomTextMessageEncoderFactory factory;
       private XmlWriterSettings writerSettings;
       private string contentType;
       
       public CustomTextMessageEncoder(CustomTextMessageEncoderFactory factory)
       {
           this.factory = factory;
           
           this.writerSettings = new XmlWriterSettings();
           this.writerSettings.Encoding = Encoding.GetEncoding(factory.CharSet);
           this.contentType = string.Format("{0}; charset={1}", 
               this.factory.MediaType, this.writerSettings.Encoding.HeaderName);
       }
[...]
}

Fortunatamente c’è un esempio MSDN che mostra tutto quello che c’è bisogna di fare (“Custom Message Encoder: Custom Text Encoder”).

L’eccezione che ho riportato sopra si verificava perché il web service non forniva alcun charset nell’header Content-Type e il client WCF assumeva erroneamente che si trattava di UTF-8. Durante la decodifica veniva incontrato il carattere ‘°’ che corrisponde al byte B0 nella codifica ISO-8859-1: tale byte non è un carattere valido in UTF-8, a meno che non sia accoppiato ad un altro byte per rappresentare un carattere, utilizzando quindi 2 byte; la codifica del carattere ‘°’ in UTF8 è la sequenza ‘C2B0’.

Qualcuno si starà chiedendo come mai una codifica chiamata UTF-8 può utilizzare 2 byte per rappresentare un carattere; cerchiamo di fare chiarezza anche su quello: UTF-8 è una codifica Unicode, ovvero permette di rappresentare tutti i caratteri Unicode con una codifica a lunghezza variabile (da 1 a 4 byte). Anche UTF16 è una codifica Unicode che, a differenza di UTF8, utilizza quasi sempre 2 byte oppure 4 byte per i caratteri meno usati. Per completezza, spendiamo due parole anche su ISO-8859-1: essa codifica 256 caratteri utilizzando sempre e solo un singolo byte.

Andrea Liberatore

Senior Support Engineer

Developer Support Core