6

I have a wcf rest service on IIS 7.5. When someone visits a part of the endpoint that doesn't exist (i.e. http://localhost/rest.svc/DOESNOTEXIST vs http://localhost/EXISTS) they are presented with a Generic WCF gray and blue error page with status code 404. However, I would like to return something like the following:

<service-response>
   <error>The url requested does not exist</error>
</service-response>

I tried configuring the custom errors in IIS, but they only work if requesting a page outside of the rest service (i.e. http://localhost/DOESNOTEXIST).

Does anyone know how to do this?

Edit After the answer below I was able to figure out I needed to create a WebHttpExceptionBehaviorElement class that implements BehaviorExtensionElement.

 public class WebHttpExceptionBehaviorElement : BehaviorExtensionElement
 {
    ///  
    /// Get the type of behavior to attach to the endpoint  
    ///  
    public override Type BehaviorType
    {
        get
        {
            return typeof(WebHttpExceptionBehavior);
        }
    }

    ///  
    /// Create the custom behavior  
    ///  
    protected override object CreateBehavior()
    {
        return new WebHttpExceptionBehavior();
    }  
 }

I was then able to reference it in my web.config file via:

<extensions>
  <behaviorExtensions>
    <add name="customError" type="Service.WebHttpExceptionBehaviorElement, Service"/>
  </behaviorExtensions>
</extensions>

And then adding

<customError /> 

to my default endpoint behaviors.

Thanks,

Jeffrey Kevin Pry

Jeffrey Kevin Pry
  • 3,266
  • 3
  • 35
  • 67

2 Answers2

6

First, create a custom behavior which subclasses WebHttpBehavior - here you will remove the default Unhandled Dispatch Operation handler, and attach your own:

public class WebHttpBehaviorEx : WebHttpBehavior
{
    public override void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
        base.ApplyDispatchBehavior(endpoint, endpointDispatcher);

        endpointDispatcher.DispatchRuntime.Operations.Remove(endpointDispatcher.DispatchRuntime.UnhandledDispatchOperation);
        endpointDispatcher.DispatchRuntime.UnhandledDispatchOperation = new DispatchOperation(endpointDispatcher.DispatchRuntime, "*", "*", "*");
        endpointDispatcher.DispatchRuntime.UnhandledDispatchOperation.DeserializeRequest = false;
        endpointDispatcher.DispatchRuntime.UnhandledDispatchOperation.SerializeReply = false;
        endpointDispatcher.DispatchRuntime.UnhandledDispatchOperation.Invoker = new UnknownOperationInvoker();

    }
}

Then. make your unknown operation handler. This class will handle the unknown operation request and generate a "Message" which is the response. I've shown how to create a plain text message. Modifying it for your purposes should be fairly straight-forward:

internal class UnknownOperationInvoker : IOperationInvoker
{
    public object[] AllocateInputs()
    {
        return new object[1];
    }


    private Message CreateTextMessage(string message)
    {
        Message result = Message.CreateMessage(MessageVersion.None, null, new HelpPageGenerator.TextBodyWriter(message));
        result.Properties["WebBodyFormatMessageProperty"] = new WebBodyFormatMessageProperty(WebContentFormat.Raw);
        WebOperationContext.Current.OutgoingResponse.ContentType = "text/html";
        return result;
    }

    public object Invoke(object instance, object[] inputs, out object[] outputs)
    {
        // Code HERE

                StringBuilder builder = new System.Text.StringBuilder();

                builder.Append("...");

                Message result = CreateTextMessage(builder.ToString());

                return result;
    }

    public System.IAsyncResult InvokeBegin(object instance, object[] inputs, System.AsyncCallback callback, object state)
    {
        throw new System.NotImplementedException();
    }

    public object InvokeEnd(object instance, out object[] outputs, System.IAsyncResult result)
    {
        throw new System.NotImplementedException();
    }

    public bool IsSynchronous
    {
        get { return true; }
    }
}

At this point you have to associate the new behavior with your service.

There are several ways to do that, so just ask if you don't already know, and i'll happily elaborate further.

Steve
  • 31,144
  • 19
  • 99
  • 122
  • 1
    in the Invoke section what do I assign to inputs and outputs? – Jeffrey Kevin Pry Jun 16 '11 at 13:01
  • Also where do I associate the web behavior in the config? this is what I have now... ... ... Thanks for your help! – Jeffrey Kevin Pry Jun 16 '11 at 13:04
  • I'll update the answer with more detail re: your comments - but, in short, see how I "return result;" in the invoke method? It's returning a type of "Message"... this is what you want. This link: http://bit.ly/AwBaK has a description of using the web config to add your new customer behavior. – Steve Jun 16 '11 at 14:04
  • Thanks... But in the invoke method you also have out object[] outputs as a parameter. It will not allow me to return a null outputs. I am returning a message. I'll await your implementation reply for associating the behavior. Thanks again for all of your help! – Jeffrey Kevin Pry Jun 16 '11 at 14:26
  • Sorry about that, I got slammed when I walked into work... glad you figured it out. – Steve Jun 16 '11 at 16:27
  • Ok, I am seriously frustrated with the whole ChannelFactory and all the crazy components. I can't reference a wcf behavior extension because I change my assembly version numbers every valid checkin, so the assembly reference would change all the freeking time. I am hosting my service as a svc within IIS. can I programmatically add this behavior to the end points from within the global.asax? – Nathan Tregillus Jul 18 '12 at 23:00
  • I have implemented this, but when my service host is getting generated, the Description.Endpoints collection is empty (count== 0) how do I add a behavior to an endpoint that doesn't exist? why doesn't the endpoint exist? – Nathan Tregillus Jul 19 '12 at 19:55
  • I figured out why the endpoints don't exist. I had to call host.AddDefaultEndpoints(); from within my custom service host factory because I am using the WebServiceHost. – Nathan Tregillus Jul 19 '12 at 20:37
  • what is HelpPageGenerator.TextBodyWriter? – Nathan Tregillus Jul 19 '12 at 20:39
  • @NathanTregillus, HelpPageGenerator is specific to my project. You'd have your own classes/whatever to do whatever you needed to do for your project. – Steve Jul 24 '12 at 17:01
1

I had a similar problem, and the other answer did lead to my eventual success, it was not the clearest of answers. Below is the way I solved this issue.

My projects setup is a WCF service hosted as a svc hosted in IIS. I could not go with the configuration route for adding the behavior, because my assembly versions change every checkin due to continuous integration.

to overcome this obsticle, I created a custom ServiceHostFactory:

using System.ServiceModel;
using System.ServiceModel.Activation;

namespace your.namespace.here
{
    public class CustomServiceHostFactory : WebServiceHostFactory
    {
        protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
        {
            ServiceHost host = base.CreateServiceHost(serviceType, baseAddresses);
            //note: these endpoints will not exist yet, if you are relying on the svc system to generate your endpoints for you
            // calling host.AddDefaultEndpoints provides you the endpoints you need to add the behavior we need.
            var endpoints = host.AddDefaultEndpoints();
            foreach (var endpoint in endpoints)
            {
                endpoint.Behaviors.Add(new WcfUnkownUriBehavior());
            }

            return host;
        }
    }
}

As you can see above, we are adding a new behavior: WcfUnknownUriBehavior. This new custom behavior's soul duty is to replace the UnknownDispatcher. below is that implementation:

using System.ServiceModel.Dispatcher;
using System.ServiceModel.Channels;
using System.ServiceModel.Web;
namespace your.namespace.here
{
    public class UnknownUriDispatcher : IOperationInvoker
    {
        public object[] AllocateInputs()
        {
            //no inputs are really going to come in,
            //but we want to provide an array anyways
            return new object[1]; 
        }

        public object Invoke(object instance, object[] inputs, out object[] outputs)
        {
            var responeObject = new YourResponseObject()
            {
                Message = "Invalid Uri",
                Code = "Error",
            };
            Message result = Message.CreateMessage(MessageVersion.None, null, responeObject);
            WebOperationContext.Current.OutgoingResponse.ContentType = "text/html";
            outputs = new object[1]{responeObject};
            return result;
        }

        public System.IAsyncResult InvokeBegin(object instance, object[] inputs, System.AsyncCallback callback, object state)
        {
            throw new System.NotImplementedException();
        }

        public object InvokeEnd(object instance, out object[] outputs, System.IAsyncResult result)
        {
            throw new System.NotImplementedException();
        }

        public bool IsSynchronous
        {
            get { return true; }
        }
    }
}

Once you have these objects specified, you can now use the new factory within your svc's "markup":

<%@ ServiceHost Language="C#" Debug="true" Service="your.service.namespace.here" CodeBehind="myservice.svc.cs"
                Factory="your.namespace.here.CustomServiceHostFactory" %>

And that should be it. as long as your object "YourResponseObject" can be serialized, it's serialized representation will be sent back to the client.

Nathan Tregillus
  • 6,006
  • 3
  • 52
  • 91