Search code examples
.netwcfproxywcf-binding

Why is my WCF proxy sending JSON with Content-Type: application/xml?


Summary

I have a WCF backend service running in an Azure app service, and I'm developing a proxy which will use the same interface but manipulate some messages for compatibility with a client's system. I know this is a bit vague, but the details aren't important.

The problem is that when the proxy, running in IIS Express from Visual Studio 17, calls the backend it is doing so incorrectly, and the backend rejects the request. (Curiously the precise nature of the rejection differs between the real service and the minimal reproducible test case I've created for this post, but the difference is late enough that I don't believe it to be relevant).

By adding trace listeners to System.Net I can observe that the call made by the proxy to the backend has a body in JSON but headers which claim that it's XML:

System.Net Verbose: 0 : [4140] Data from ConnectStream#29742526::Write
System.Net Verbose: 0 : [4140] 00000000 : 7B 22 42 61 72 22 3A 22-42 2D 50 72 6F 78 69 65 : {"Bar":"B-Proxie
System.Net Verbose: 0 : [4140] 00000010 : 64 22 2C 22 46 6F 6F 22-3A 22 41 22 7D          : d","Foo":"A"}
System.Net Verbose: 0 : [4140] Exiting ConnectStream#29742526::Write() 
System.Net Verbose: 0 : [4140] Entering ConnectStream#29742526::Close()
System.Net Verbose: 0 : [4140] Exiting ConnectStream#29742526::Close() 
System.Net Verbose: 0 : [4140] Entering HttpWebRequest#45658036::GetResponse()
System.Net Information: 0 : [4140] HttpWebRequest#45658036 - Request: POST /DemoService.svc/DoTheThing HTTP/1.1

System.Net Information: 0 : [4140] ConnectStream#29742526 - Sending headers
{
Content-Type: application/xml; charset=utf-8
Host: wcfproxydemo.azurewebsites.net
Content-Length: 29
Expect: 100-continue
Accept-Encoding: gzip, deflate
Connection: Close
}.

Can I change some configuration to get it to send JSON with the correct Content-Type header? I'd prefer not to have to switch everything over to XML.

Details

My minimal reproducible test case is available on Github. The only thing I've elided there is the publish profile to push the backend to Azure.

The various projects share an interface project, and there's no auto-generated service reference. (That might be a possible way forward, but it seems rather heavy-weight, so I'd prefer a config-based solution). The WCF config for the three components is

Backend

  <system.serviceModel>
    <bindings>
      <customBinding>
        <binding name="rest-https-nokeepalive">
          <webMessageEncoding />
          <httpsTransport manualAddressing="true" allowCookies="false" keepAliveEnabled="false" maxBufferSize="10000000" maxReceivedMessageSize="10000000" maxBufferPoolSize="10000000" />
        </binding>
      </customBinding>
    </bindings>
    <services>
      <service name="Backend.DemoService" behaviorConfiguration="ServiceBehaviour">
        <endpoint binding="customBinding" bindingConfiguration="rest-https-nokeepalive" contract="Interface.IDemoService" behaviorConfiguration="web" />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ServiceBehaviour">
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="web">
          <webHttp />
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="false" multipleSiteBindingsEnabled="true" />
  </system.serviceModel>

Proxy

  <system.serviceModel>
    <bindings>
      <customBinding>
        <binding name="rest-https-nokeepalive">
          <webMessageEncoding />
          <httpsTransport manualAddressing="true" allowCookies="false" keepAliveEnabled="false" maxBufferSize="10000000" maxReceivedMessageSize="10000000" maxBufferPoolSize="10000000" />
        </binding>
      </customBinding>
    </bindings>
    <services>
      <service name="Proxy.DemoService" behaviorConfiguration="ServiceBehaviour">
        <endpoint binding="customBinding" bindingConfiguration="rest-https-nokeepalive" contract="Interface.IDemoService" behaviorConfiguration="web" />
      </service>
    </services>
    <client>
      <endpoint name="Backend" address="https://wcfproxydemo.azurewebsites.net/DemoService.svc" binding="customBinding" bindingConfiguration="rest-https-nokeepalive" contract="Interface.IDemoService" behaviorConfiguration="web" />
    </client>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ServiceBehaviour">
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="web">
          <webHttp />
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="false" multipleSiteBindingsEnabled="true" />
    <diagnostics>
      <messageLogging logEntireMessage="true" logMalformedMessages="true" logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="true" maxMessagesToLog="3000" maxSizeOfMessageToLog="2000" />
    </diagnostics>
  </system.serviceModel>

Test frontend

  <system.serviceModel>
    <bindings>
      <customBinding>
        <binding name="rest-https-nokeepalive">
          <webMessageEncoding />
          <httpsTransport manualAddressing="true" allowCookies="false" keepAliveEnabled="false" maxBufferSize="10000000" maxReceivedMessageSize="10000000" maxBufferPoolSize="10000000" />
        </binding>
      </customBinding>
    </bindings>
    <client>
      <endpoint name="Direct" address="https://wcfproxydemo.azurewebsites.net/DemoService.svc" binding="customBinding" bindingConfiguration="rest-https-nokeepalive" contract="Interface.IDemoService" behaviorConfiguration="web" />
      <endpoint name="Proxied" address="https://localhost:44388/DemoService.svc" binding="customBinding" bindingConfiguration="rest-https-nokeepalive" contract="Interface.IDemoService" behaviorConfiguration="web" />
    </client>
    <behaviors>
      <endpointBehaviors>
        <behavior name="web">
          <webHttp />
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <diagnostics>
      <messageLogging logEntireMessage="true" logMalformedMessages="true" logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="true" maxMessagesToLog="3000" maxSizeOfMessageToLog="2000"/>
    </diagnostics>
  </system.serviceModel>

A direct connection from the frontend to the backend works fine. A proxied connection fails because the backend isn't happy with the request it receives from the proxy, for obvious reasons.


Solution

  • I downloaded your code and reproduced your problem, the second request reports a 400 bad request error. I re-construct your project and use the client proxy class to invoke the service in the proxy project and the Tester project, and when I remove the interface attribute in the proxy project(I use the three interfaces, although there are identical), it works.

    [WebInvoke(Method = "POST")]
    ConsoleApp1.ServiceReference1.DemoResponse DoTheThing(ConsoleApp1.ServiceReference1.DemoRequest request);
    

    Besides, we could change the second outgoing request content type by using WebOperationContext class in the Proxy project, please refer to the below code snippets.

    public DemoResponse DoTheThing(DemoRequest request)
        {
            request.Bar += "-Proxied";
            IDemoService service = factory.CreateChannel();
            using (OperationContextScope scope = new OperationContextScope((IContextChannel)service))
            {
                WebOperationContext woc = WebOperationContext.Current;
                woc.OutgoingRequest.ContentType = "application/json; charset=utf-8";
                var result = service.DoTheThing(request);
                try
                {
                    return result;
                }
                finally
                {
                    if (result is System.ServiceModel.ICommunicationObject client)
                    {
                        if (client.State == System.ServiceModel.CommunicationState.Faulted) client.Abort();
                        else client.Close();
                    }
                }
            }
        }
    

    Feel free to let me know if there is anything I can help with.