AJAX-enabled WCF Services: “System.ArgumentException: Specified value has invalid Control characters.”

Probabilmente sapete già che è possibile creare dei servizi WCF invocabili da ASP.NET AJAX; l’obiettivo di questo post non è di spiegare come farlo: quello che ritengo più importante adesso è chiarire lo scenario in cui il problema che sto per illustrare può verificarsi.

Un AJAX-enabled WCF Service altro non è che un WCF service che espone degli endpoint compatibili con AJAX, permettendo ad un client ASP.NET AJAX di invocarli senza alcun problema. Ecco un esempio di configurazione di un servizio WCF che supporta client AJAX:

 <system.serviceModel>
<services>
        <service name="Microsoft.Ajax.Samples.CalculatorService">
            <endpoint address=""
                behaviorConfiguration="AspNetAjaxBehavior" 
                binding="webHttpBinding"
                contract="Microsoft.Ajax.Samples.ICalculator" />
        </service>
    </services>
    <behaviors>
        <endpointBehaviors>
            <behavior name="AspNetAjaxBehavior">
                <enableWebScript />
            </behavior>
        </endpointBehaviors>
    </behaviors>
</system.serviceModel>

Come si può notare, viene definito un EndPointBehavior che introduce enableWebScript, il quale contribuisce a creare un endpoint compatibile con ASP.NET AJAX qualora il binding utilizzato sia webHttpBinding. 
Ora passiamo al nostro problema, appena introdotto dall’errore riportato nel titolo. Talvolta, un AJAX-enabled WCF Service può risultare apparentemente unresponsive, presentando la seguente eccezione nei WCF traces eventualmente abilitati lato servizio:

<Exception>

<ExceptionType>System.ArgumentException, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>

<Message>Specified value has invalid Control characters.

Parameter name: value</Message>

<StackTrace>

at System.Net.WebHeaderCollection.CheckBadChars(String name, Boolean isHeaderValue)

at System.Net.WebHeaderCollection.Add(String name, String value)

at System.Collections.Specialized.NameValueCollection.Add(NameValueCollection c)

at System.ServiceModel.Channels.HttpRequestMessageProperty.get_Headers()

at System.ServiceModel.Description.WebScriptClientGenerator.Get(Message message)

at SyncInvokeGet(Object , Object[] , Object[] )

at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]&amp; outputs)

at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc&amp; rpc)

at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc&amp; rpc)

at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc&amp; rpc)

at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)

</StackTrace>

<ExceptionString>System.ArgumentException: Specified value has invalid Control characters.

Parameter name: value

at System.Net.WebHeaderCollection.CheckBadChars(String name, Boolean isHeaderValue)

at System.Net.WebHeaderCollection.Add(String name, String value)

at System.Collections.Specialized.NameValueCollection.Add(NameValueCollection c)

at System.ServiceModel.Channels.HttpRequestMessageProperty.get_Headers()

at System.ServiceModel.Description.WebScriptClientGenerator.Get(Message message)

at SyncInvokeGet(Object , Object[] , Object[] )

at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]&amp; outputs)

at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc&amp; rpc)

at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc&amp; rpc)

at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc&amp; rpc)

at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)</ExceptionString>

</Exception>

Questo problema normalmente non è sistematico: si verifica dopo un tempo variabile, apparentemente senza alcuna particolare condizione che possa preannunciarlo.

In realtà accade quanto segue: se una richiesta contenente caratteri di controllo non ASCII in un qualunque header HTTP viene fatta tramite l’endpoint AJAX al fine di ottenere un JavaScript proxy, quest’ultimo si verrà trovare in uno stato corrotto, pertanto tutte le successive richieste, anche se buone, non andranno a buon fine.

Tale problema può verificarsi anche con .NET 3.5 SP1, tuttavia dovrebbe essere risolto dalla successiva versione del Framework.

La notizia buona è che esiste un workaround, quella cattiva è che il workaround è implementabile esclusivamente modificando il codice del servizio WCF.

Si tratta di aggiungere un error handler a tutti i canali che hanno un endpoint JS; tuttavia l’introduzione di questo error handler è complicata dal fatto che il channel dispatcher per tali endpoint viene creato soltanto dopo la open del ServiceHost. Questo implica che dobbiamo creare un ServiceHostFactory custom che introduce il nostro error handler e modificare il .svc del servizio in modo da farlo puntare al nuovo esso. Più o meno così:

 <%@ServiceHost language=c# Debug="true" Service="<the_actual_service_name>" Factory="MyTest.MyServiceHostFactory" %>

namespace MyTest
{
    using System;
    using System.ServiceModel;
    using System.ServiceModel.Activation;
    using System.ServiceModel.Web;
    using System.ServiceModel.Dispatcher;
    using System.ServiceModel.Description;
    using System.ServiceModel.Channels;

    public class MyServiceHostFactory : ServiceHostFactory
    {
        protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
        {
            return new MyServiceHost(serviceType, baseAddresses);
        }
    }
    public class MyServiceHost : ServiceHost
    {
        public MyServiceHost(Type serviceType, params Uri[] baseAddresses)
            : base(serviceType, baseAddresses)
        {
        }
        protected override void InitializeRuntime()
        {
            base.InitializeRuntime();
            foreach (ChannelDispatcher cd in this.ChannelDispatchers)
            {
                bool isJs = false;
                foreach (EndpointDispatcher ep in cd.Endpoints)
                {
                    string epAddress = ep.EndpointAddress.Uri.ToString();
                    if (epAddress.EndsWith("/js") || epAddress.EndsWith("/jsdebug"))
                    {
                        isJs = true;
                    }
                }
                if (isJs)
                {
                    cd.ErrorHandlers.Add(new MyErrorHandler());
                }
            }
        }
    }
    public class MyErrorHandler : IErrorHandler
    {
        #region IErrorHandler Members

        public bool HandleError(Exception error)
        {
            return true;
        }

        public void ProvideFault(Exception error, System.ServiceModel.Channels.MessageVersion version, ref System.ServiceModel.Channels.Message fault)
        {
            FaultException<string> ex = new FaultException<string>(error.Message);
            MessageFault messageFault = ex.CreateMessageFault();
            fault = Message.CreateMessage(version, messageFault, "https://my.fault/");
        }

        #endregion
    }
}

Come si può osservare, questo codice semplicemente aggiunge l’error handler  ad ogni endpoint JS, o meglio ad ogni endpoint il cui indirizzo termina con “/js” o “/jsdebug”.

Non appena introdotto il nuovo ServiceHostFactory, il problema non si farà più vivo. Provare per credere!

Alla prossima!

Andrea Liberatore

Senior Support Engineer

Developer Support Core