L’apertura in Internet Explorer di un WCF .svc in HTTPS dà HTTP 400 Bad request

Recentemente ho lavorato su un caso piuttosto singolare che mi piacerebbe condividere.
Un cliente aveva configurato un WCF service in  IIS6 con la transport security e ogni volta che provava ad accedere alla pagina del WSDL otteneva un 400 Bad Request. Lo stesso problema si verificava semplicemente aprendo l’URL del .svc in Internet Explorer; disabilitando la transport security l’errore scompariva e sia il .svc che la pagina del WSDL potevano essere accedute con successo.
La soluzione del problema era tutto sommato banale e la dichiarerò dall’inizio.
Il web.config riportava un service endpoint molto simile a questo:

 <endpoint address=”https://andreal3.microsoft.com/webs/webs.svc” binding=”wsHttpBinding” bindingConfiguration=”WsSecureWithAuthBinding” ..>

Come si può notare, l’address dell’endpoint era assoluto. In base a quanto documentato qui:

“There are two ways to specify endpoint addresses for a service in WCF. You can specify an absolute address for each endpoint associated with the service or you can provide a base address for the ServiceHost of a service and then specify an address for each endpoint associated with this service that is defined relative to this base address. You can use each of these procedures to specify the endpoint addresses for a service in either configuration or code. If you do not specify a relative address, the service uses the base address. You can also have multiple base addresses for a service, but each service is allowed only one base address for each transport. If you have multiple endpoints, each of which is configured with a different binding, their addresses must be unique. Endpoints that use the same binding but different contracts can use the same address.

When hosting with IIS, you do not manage the ServiceHost instance yourself. The base address is always the address specified in the .svc file for the service when hosting in IIS. So you must use relative endpoint addresses for IIS-hosted service endpoints. Supplying a fully-qualified endpoint address can lead to errors in the deployment of the service.

E’ bastato quindi impostare un address relativo per risolvere il problema:

 <endpoint address=”” binding=”wsHttpBinding” bindingConfiguration=”WsSecureWithAuthBinding”…. >

Tuttavia questa soluzione non andava bene al cliente, che avendo il server stand-alone, otteneva nel WSDL il seguente URL di servizio: “https://andreal3/webs/webs.svc”, ma l’obiettivo era di avere “https://andreal3.microsoft.com/webs/webs.svc”.  E’ necessario sapere che un endpoint prevede la proprietà “listenUri” oltre ad “address”: la listenUri è, come il nome potrebbe suggerire, l’URI al quale il servizio è in ascolto. La listenUri è impostata con lo stesso valore dell’address by default, ma è possibile configurarla separatamente, in questo modo:

 <endpoint address=”https://andreal3.microsoft.com/webs/webs.svc” binding=”wsHttpBinding” bindingConfiguration=”WsSecureWithAuthBinding” 
listenURI=”” ..>

In questo modo otteniamo l’indirizzo desiderato nel WSDL e nello stesso tempo il servizio è in ascolto sul baseAddress di default (che nell’esempio sarebbe https://andreal/webs/webs.svc).

Una domanda a cui bisogna a questo punto rispondere è: qual è il nesso tra l’endpoint address e un 400 Bad Request? Ho trascorso un po’ di tempo debuggando una repro fornita dal cliente e ho riscontrato che l’errore si verificava in questo punto del codice (.NET Reflector):

 public Message ParseIncomingMessage(out Exception requestException);

Declaring Type: System.ServiceModel.Channels.HttpInput 
Assembly: System.ServiceModel, Version=3.0.0.0 

public Message ParseIncomingMessage(out Exception requestException)
{
    Message message = null;
    Message message2;
    requestException = null;
    bool flag = true;
    try
    {
        this.ValidateContentType();
        ServiceModelActivity activity = null;
        if (DiagnosticUtility.ShouldUseActivity && ((ServiceModelActivity.Current == null) || (ServiceModelActivity.Current.ActivityType != ActivityType.ProcessAction)))
        {
            activity = ServiceModelActivity.CreateBoundedActivity(true);
        }
        using (activity)
        {
            if (DiagnosticUtility.ShouldUseActivity && (activity != null))
            {
                ServiceModelActivity.Start(activity, SR.GetString("ActivityProcessingMessage", new object[] { TraceUtility.RetrieveMessageNumber() }), ActivityType.ProcessMessage);
            }
            if (!this.HasContent)
            {
------------->               if (this.messageEncoder.MessageVersion != MessageVersion.None)
                {
                    return null; //failing case
                }
                message = new NullMessage();
            }
            else if (this.streamed)
            {
                message = this.ReadStreamedMessage(this.InputStream);
            }
            else if (this.ContentLength == -1L)
            {
                message = this.ReadChunkedBufferedMessage(this.InputStream);
            }
            else
            {
                message = this.ReadBufferedMessage(this.InputStream);
            }
            requestException = this.ProcessHttpAddressing(message);
            flag = false;
            message2 = message;
        }
    }
    finally
    {
        if (flag)
        {
            this.Close();
        }
    }
    return message2;
}

In caso di errore potevo sempre osservare che la messageVersion era “soap12Addressing10” invece di “none”: essendo che la pagina .svc e quella del wsdl vengono ovviamente scaricate con  una GET, nessun messaggio SOAP è atteso nel body, di conseguenza la messageVersion è “none”. L’effetto del valore di ritorno null della ParseIncomingMessage è una ProtocolException sollevata dal chiamante, innescando il processo che porta ad un HTTP 400 Bad request.

A questo punto bisognava capire perché la messageVersion era impostata a “”soap12Addressing10” invece che a “none”. Ho verificato che, quando viene configurata la transport security, abbiamo i seguenti tre HttpsChannelListener:

1) https://localhost/webs/webs.svc (soap12Addressing10)

2) https://andreal3/webs/webs.svc/mex

3) https://andreal3/webs/webs.svc (None)

A questo punto era evidente che il servizio stava selezionando l’HttpsChannelListener 1), mentre avrebbe dovuto selezionare il 3) per gestire le GET. La selezione del listener avviene sulla base della web request, precisamente in questo metodo:

 Declaring Type: System.ServiceModel.Channels.HttpTransportManager 
Assembly: System.ServiceModel, Version=3.0.0.0

protected bool TryLookupUri(Uri requestUri, string requestMethod, HostNameComparisonMode hostNameComparisonMode, out HttpChannelListener listener)
{
    listener = null;
    if (requestMethod == null)
    {
        requestMethod = string.Empty;
    }
    lock (this.addressTables)
    {
        UriPrefixTable<HttpChannelListener> table;
        HttpChannelListener item = null;
        if (((requestMethod.Length > 0) && this.addressTables.TryGetValue(requestMethod, out table)) && (table.TryLookupUri(requestUri, hostNameComparisonMode, out item) && (string.Compare(requestUri.AbsolutePath, item.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase) != 0)))
        {
            item = null;
        }
        if (this.addressTables.TryGetValue(string.Empty, out table) && table.TryLookupUri(requestUri, hostNameComparisonMode, out listener))
        {
            if ((item != null) && (item.Uri.AbsoluteUri.Length >= listener.Uri.AbsoluteUri.Length))
            {
                listener = item;
            }
        }
        else
        {
            listener = item;
        }
    }
    return (listener != null);
}

Questo “if” : if ((item != null) && (item.Uri.AbsoluteUri.Length >= listener.Uri.AbsoluteUri.Length))

è la causa del problema. Senza entrare troppo nei dettagli, l’URI ricevuto nella GET era “https://andreal3.microsoft.com/webs/webs.svc” veniva usato per confrontarne la lunghezza con “https://localhost/webs/webs.svc” e questo faceva sì che fosse selezionato il listener 1) (“https://localhost/webs/webs.svc” appunto).

Semplicemente impostando il service endpoint address come stringa vuota, veniva usato il base address e l’URI risultava “https://andreal3/webs/webs.svc”. Questo URI è più coto di “https://localhost/webs/webs.svc” e quindi veniva eseguito l’altro ramo dell’if, aggirando il problema.

Naturalmente non si può parlare di bug, perché è espressamente documentato che quando si “hosta” un WCF service in IIS bisogna usare soltanto endpoint address relativi per evitare problemi, tuttavia questo punto potrebbe sfuggire e non sarebbe così immediato risalire alla causa dall’effetto (400 Bad Request). Mi scuso se non sono stato sufficientemente accurato, ma l’obiettivo d iquesto post era di fornire degli spunti sopratutto per sapere dove andare a guardare qualora ci trovassio a debuggar eun problema con i listener.

Nota: senza transport security il problema non si verificava perché era presente solo un listener con messageVersion “none” al momento della GET.

Nota2: per debuggare il problema ho lavorato con .NET Reflector e WinDBG; era facile lavorare con il debugger attaccato in quanto il problema si riproduceva sistematicamente.