Вызов jQuery success вызывается с пустым ответом, когда метод WCF создает исключение - PullRequest
16 голосов
/ 06 декабря 2010

Я рву на себе волосы, так что терпите меня (это длинный пост).

Базовая информация

  • ASP.NET 3.5 со службой WCF в режиме совместимости с ASP.NET
  • Использование jQuery с этим прокси-сервером для запросов AJAX
  • Пользовательская реализация IErrorHandler и IServiceBehavior для перехвата исключений и предоставления ошибок, которые сериализуются в JSON
  • Я тестирую локально с помощью Cassini (я видел несколько потоков, в которых говорится о проблемах, возникающих при локальной отладке, но которые хорошо работают в производственной среде).

Проблема, с которой я сталкиваюсь, заключается в том, что всякий раз, когда из моей службы WCF генерируется исключение, запускается обработчик success вызова $.ajax. Ответ пуст, текст статуса «Успешно» и код ответа 202 / Принято.

Реализация IErrorHandler привыкает, потому что я могу пройтись по ней и посмотреть, как создается FaultMessage. В итоге происходит то, что обратный вызов success выдает ошибку, потому что текст ответа пуст, когда он ожидает строку JSON. Обратный вызов error никогда не срабатывает.

Одной вещью, которая обеспечила небольшую проницательность, было удаление опции enableWebScript из поведения конечной точки. Когда я сделал это, произошли две вещи:

  1. Ответы больше не были завернуты (т. Е. Нет { d: "result" }, просто "result").
  2. Срабатывает обратный вызов error, но ответом является только HTML для желтого экрана смерти 400 / Bad Request от IIS, а не моя сериализованная ошибка.

Я пробовал столько же вещей, что и в топ-10 или более результатов Google, в отношении случайных комбинаций ключевых слов "jquery ajax asp.net wcf faultcontract json", поэтому, если вы планируете поискать в поиске ответа, не заморачивайся Я надеюсь, что кто-то в SO сталкивался с этой проблемой раньше.

В конечном итоге я хочу достичь:

  1. Уметь использовать любой тип Exception в методе WCF
  2. Используйте FaultContact
  3. Отловить исключения в ShipmentServiceErrorHandler
  4. Вернуть сериализованный ShipmentServiceFault (как JSON) клиенту.
  5. Вызвать обратный вызов error, чтобы я мог обработать элемент 4.

Возможно связано с:

<ч />

Обновление 1

Я изучил выходные данные из трассировки действия System.ServiceModel, и в какой-то момент после вызова метода UpdateCountry выдается исключение, сообщение

Сервер вернул недопустимую ошибку SOAP.

и все. Внутреннее исключение жалуется на то, что сериализатор ожидает другого корневого элемента, но я не могу ничего расшифровать из него.

<ч />

Обновление 2

Так что с еще большим количеством возни, я получил кое-что для работы, хотя и не так, как считал бы идеальным. Вот что я сделал:

  1. Удален параметр <enableWebScript /> из раздела поведения конечной точки web.config.
  2. Удален атрибут FaultContract из метода обслуживания.
  3. Реализовал подкласс WebHttpBehavior (называемый ShipmentServiceWebHttpBehavior) и переопределил функцию AddServerErrorHandlers, добавив ShipmentServiceErrorHandler.
  4. Изменено ShipmentServiceErrorHandlerElement для возврата экземпляра типа ShipmentServiceWebHttpBehavior вместо самого обработчика ошибок.
  5. Переместил строку <errorHandler /> из раздела поведения службы web.config в раздел поведения конечной точки.

Это не идеально, потому что теперь WCF игнорирует BodyStyle = WebMessageBodyStyle.WrappedRequest, который я хочу в моих методах обслуживания (хотя теперь я могу вообще его опустить). Мне также пришлось изменить некоторый код в прокси службы JS, потому что он искал объект обертки ({ d: ... }) в ответах.

<ч />

Вот весь соответствующий код (объект ShipmentServiceFault довольно понятен).

Служба

Мой сервис очень прост (урезанная версия):

[ServiceContract(Namespace = "http://removed")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class ShipmentService
{

    [OperationContract]
    [WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.WrappedRequest)]
    [FaultContract(typeof(ShipmentServiceFault))]
    public string UpdateCountry(Country country)
    {
        var checkName = (country.Name ?? string.Empty).Trim();
        if (string.IsNullOrEmpty(checkName))
            throw new ShipmentServiceException("Country name cannot be empty.");

        // Removed: try updating country in repository (works fine)

        return someHtml; // new country information HTML (works fine)
    }

}

Обработка ошибок

Реализация IErrorHandler, IServiceBehavior выглядит следующим образом:

public class ShipmentServiceErrorHandlerElement : BehaviorExtensionElement
{
    protected override object CreateBehavior()
    {
        return new ShipmentServiceErrorHandler();
    }

    public override Type BehaviorType
    {
        get
        {
            return typeof(ShipmentServiceErrorHandler);
        }
    }
}

public class ShipmentServiceErrorHandler : IErrorHandler, IServiceBehavior
{
    #region IErrorHandler Members

    public bool HandleError(Exception error)
    {
        // We'll handle the error, we don't need it to propagate.
        return true;
    }

    public void ProvideFault(Exception error, System.ServiceModel.Channels.MessageVersion version, ref System.ServiceModel.Channels.Message fault)
    {
        if (!(error is FaultException))
        {
            ShipmentServiceFault faultDetail = new ShipmentServiceFault
            {
                Reason = error.Message,
                FaultType = error.GetType().Name
            };

            fault = Message.CreateMessage(version, "", faultDetail, new DataContractJsonSerializer(faultDetail.GetType()));

            this.ApplyJsonSettings(ref fault);
            this.ApplyHttpResponseSettings(ref fault, System.Net.HttpStatusCode.InternalServerError, faultDetail.Reason);
        }
    }

    #endregion

    #region JSON Exception Handling

    protected virtual void ApplyJsonSettings(ref Message fault)
    {
        // Use JSON encoding  
        var jsonFormatting = new WebBodyFormatMessageProperty(WebContentFormat.Json);

        fault.Properties.Add(WebBodyFormatMessageProperty.Name, jsonFormatting);
    }

    protected virtual void ApplyHttpResponseSettings(ref Message fault, System.Net.HttpStatusCode statusCode, string statusDescription)
    {
        var httpResponse = new HttpResponseMessageProperty()
        {
            StatusCode = statusCode,
            StatusDescription = statusDescription
        };

        httpResponse.Headers[HttpResponseHeader.ContentType] = "application/json";
        httpResponse.Headers["jsonerror"] = "true";

        fault.Properties.Add(HttpResponseMessageProperty.Name, httpResponse);
    }

    #endregion

    #region IServiceBehavior Members

    public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    {
        // Do nothing
    }

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
    {
        IErrorHandler errorHandler = new ShipmentServiceErrorHandler();

        foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers)
        {
            ChannelDispatcher channelDispatcher = channelDispatcherBase as ChannelDispatcher;

            if (channelDispatcher != null)
            {
                channelDispatcher.ErrorHandlers.Add(errorHandler);
            }
        }
    }

    public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
    {
        // Do nothing
    }

    #endregion
}

JavaScript

Вызов метода WCF начинается с:

    function SaveCountry() {
        var data = $('#uxCountryEdit :input').serializeBoundControls();
        ShipmentServiceProxy.invoke('UpdateCountry', { country: data }, function(html) {
            $('#uxCountryGridResponse').html(html);
        }, onPageError);
    }

Сервисный прокси, о котором я упоминал ранее, заботится о многих вещах, но в основном мы попадаем сюда:

$.ajax({
    url: url,
    data: json,
    type: "POST",
    processData: false,
    contentType: "application/json",
    timeout: 10000,
    dataType: "text",  // not "json" we'll parse
    success: function(response, textStatus, xhr) {

    },
    error: function(xhr, status) {                

    }
});

Конфигурация

Я чувствую, что проблемы могут лежать здесь, но я попробовал почти каждую комбинацию настроек из любого места, которое я могу найти в сети, где есть пример.

<system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
    <behaviors>
        <endpointBehaviors>
            <behavior name="Removed.ShipmentServiceAspNetAjaxBehavior">
                <webHttp />
                <enableWebScript />
            </behavior>
        </endpointBehaviors>
        <serviceBehaviors>
            <behavior name="Removed.ShipmentServiceServiceBehavior">
                <serviceMetadata httpGetEnabled="true"/>
                <serviceDebug includeExceptionDetailInFaults="false"/>
                <errorHandler />
            </behavior>
        </serviceBehaviors>
    </behaviors>
    <services>
        <service name="ShipmentService" behaviorConfiguration="Removed.ShipmentServiceServiceBehavior">
            <endpoint address="" 
                behaviorConfiguration="Removed.ShipmentServiceAspNetAjaxBehavior" 
                binding="webHttpBinding" 
                contract="ShipmentService" />
        </service>
    </services>
    <extensions>
        <behaviorExtensions>
            <add name="errorHandler" type="Removed.Services.ShipmentServiceErrorHandlerElement, Removed, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
        </behaviorExtensions>
    </extensions>
</system.serviceModel>

Примечания

Я заметил, что эти вопросы получают несколько избранных. Я нашел решение этой проблемы, и я надеюсь дать ответ, когда найду время. Оставайтесь с нами!

Ответы [ 5 ]

3 голосов
/ 16 июня 2011

Я не знаком с ASP или WCF, но я довольно знаком с jQuery.Единственное, что мне запомнилось по поводу вашего вопроса, это то, что ваш сервис возвращает 202 Success, когда выдается исключение.jQuery выбирает, какой обратный вызов вызывать (success или error), основываясь на коде состояния HTTP, который возвращается с сервера.202 считается успешным ответом, и поэтому jQuery будет вызывать success.Если вы хотите, чтобы jQuery вызывал обратный вызов error, вам нужно, чтобы ваша служба возвращала код состояния 40x или 50x.Обратитесь к http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html для получения списка кодов состояния HTTP.

1 голос
/ 06 декабря 2010

Вы смотрели на JSON.NET ? Я использовал его для преобразования объектов в c # в JSON-дружественные строки, а затем передавал его обратно по проводам моему клиенту, где я анализировал его в объект JSON. В конце концов я избавился от этого и пошел на JSON2 для stringify. Вот мой вызов ajax, который я использую:

function callScriptMethod(url, jsonObject, callback, async) {

    callback = callback || function () { };
    async = (async == null || async);

    $.ajax({
        type: 'POST',
        contentType: 'application/json; charset=utf-8',
        url: url,
        data: JSON.stringify(jsonObject),
        dataType: 'json',
        async: async,
        success: function (jsonResult) {
            if ('d' in jsonResult)
                callback(jsonResult.d);
            else
                callback(jsonResult);
        },
        error: function () {
            alert("Error calling '" + url + "' " + JSON.stringify(jsonObject));
            callback([]);
        }
    });
}
1 голос
/ 06 декабря 2010

У меня были те же симптомы с другим сценарием, так что это может или не может помочь.

Вот краткое описание того, что я делал, и нашего решения:

Я писал вREST-реализация службы WCF, которую мы размещаем на классической странице ASP.Я обнаружил, что должен был установить входные данные как поток и прочитать их, избавившись от потока, когда закончил.Я полагаю, что именно в этот момент я получил ответ 202 с текстом «успеха», как вы описали.Я обнаружил, что, не избавляясь от потока, я получал ответ, который ожидал для условий ошибки.

Вот сводка окончательного кода:

[WebHelp(Comment="Expects the following parameters in the post data:title ...etc")] 
    public int SaveBook(Stream stream)
    {
        NameValueCollection qString;
        StreamReader sr = null;
        string s;
        try
        {
            /**************************************************************************************
             * DO NOT CALL DISPOSE ON THE STREAMREADER OR STREAM                                  *
             * THIS WILL CAUSE THE ERROR HANDLER TO RETURN A PAGE STATUS OF 202 WITH NO CONTENT   *
             * IF THERE IS AN ERROR                                                               *

             * ***********************************************************************************/
            sr = new StreamReader(stream);
            s = sr.ReadToEnd();
            qString = HttpUtility.ParseQueryString(s);

            string title = qString["title"];

            //Do what we need

            //Then Return something
            int retRecieptNum = UtilitiesController.SubmitClientEntryRequest(entryReq);                

            return retRecieptNum;
        }
        catch (Exception ex)
        {
            throw new WebProtocolException(System.Net.HttpStatusCode.Forbidden, ex.Message, this.GetExceptionElement(true, "BookRequest", ex.Message), false, ex);
        }
        finally
        {

        }            
    }

Надеюсь, это поможетвам, может быть, попробуйте использовать поток и посмотреть, как это происходит.

0 голосов
/ 12 июня 2011

У меня была похожая проблема с WCF и использованием совместимости с ASP.NET, поскольку я интегрировал MVC и WCF в свое решение. То, что я хотел бы сделать, это бросить WebFaultException, а затем проверить состояние ответа на принимающей стороне (либо Java, либо другой клиент .NET). Ваша пользовательская ошибка может затем сгенерировать это, если WebOperationContext.Current не равен NULL. Вы, наверное, уже знаете об этом, но просто подумали, что я это выброшу.

throw new WebFaultException(HttpStatusCode.BadRequest);
0 голосов
/ 06 декабря 2010

Вот еще один выстрел.Я оставлю свою первоначальную попытку, если это решение поможет кому-то другому.

Чтобы запустить условие ошибки для вызова $ .ajax, вам понадобится код ошибки в вашем ответе

   protected virtual void ApplyHttpResponseSettings(ref Message fault, System.Net.HttpStatusCode statusCode, string statusDescription) 
    { 
        var httpResponse = new HttpResponseMessageProperty() 
        { 
            //I Think this could be your problem, if this is not an error code
            //The error condition will not fire
            //StatusCode = statusCode, 
            //StatusDescription = statusDescription 

            //Try forcing an error code
            StatusCode = System.Net.HttpStatusCode.InternalServerError;
        }; 

        httpResponse.Headers[HttpResponseHeader.ContentType] = "application/json"; 
        httpResponse.Headers["jsonerror"] = "true"; 

        fault.Properties.Add(HttpResponseMessageProperty.Name, httpResponse); 
    } 

Вотнадеюсь, что мое второе негодование будет для тебя более полезным!

...