Skip to content

Latest commit

 

History

History
266 lines (233 loc) · 9.83 KB

improved-exceptions.md

File metadata and controls

266 lines (233 loc) · 9.83 KB

Supporting error status codes and customized exceptions

The issues today

  • AutoRest treats every explicitly described status code of an operation as a successful response. Error codes if any, fall under the generic bucket of default. There is no way to indicate specific error codes that can be expected from the service for a given operation in case of an unexpected error.
  • There is limited support for customized exceptions. An error model, ErrorModel returned from the service is set as the Body property of ErrorModelException class which in turn is derived from the RestException class.
    • The exception information displayed does not contain information sent by the service in the ErrorModel object.
    • There is no way to create meaningful exception classes themselves since they are autogenerated by AutoRest. For eg., it would be natural and user friendly if the underlying base class for the exceptions were something like FileNotFoundException.
    • Certain operation responses do not have a body parameter (eg.: HEAD operations); modeling exceptions for such operations with a body property is unintuitive and confusing.

Proposed solutions

  1. Adding an extension to indicate error response codes for operations.

    "responses": {
        "200": {
        "description": "",
            "schema": {
                "$ref": "#/definitions/Pet"
            }
        },
        "400": {
            "description": "Bad Request",
            "schema":{
                "$ref":"#/definitions/BadRequestErrorModel"
            },
            "x-ms-error-response":true
        },
        "404": {
            "description": "Not found error",
            "schema":{
                "$ref":"#/definitions/NotFoundErrorModel"
            },
            "x-ms-error-response":true
        },
        "default": {
            "description": "Default errors",
            "schema":{
                "$ref":"#/definitions/DefaultErrorModel"
            }
        }
    }

    AutoRest can generate code for each error status code (which is indicated by x-ms-error-response above) and deserialize the error model returned by the server independently.

  2. Generating Get methods for all properties of the underlying error model. For example, if in the above example, NotFoundError were defined as

    "NotFoundError":{
        "properties":{
            "resourceName":{
            "type":"string"
            }
        },
        "allOf":[{
            "$ref":"#/definitions/BaseError"
        }]
      },
      "BaseError":{
        "properties":{
            "someBaseProp":{
                "type":"string"
            }
        }
      }

    The models would be designed as:

    public partial class BaseError
    {
        public BaseError()
        {
            CustomInit();
        }
    
        public BaseError(string someBaseProp = default(string))
        {
            SomeBaseProp = someBaseProp;
            CustomInit();
        }
    
        partial void CustomInit();
    
        [JsonProperty(PropertyName = "someBaseProp")]
        public string SomeBaseProp { get; set; }
    
    }

    and

    public partial class NotFoundError : BaseError
    {
        public NotFoundError()
        {
            CustomInit();
        }
    
        public NotFoundError(string someBaseProp = default(string), string resourceName = default(string))
            : base(someBaseProp)
        {
            ResourceName = resourceName;
            CustomInit();
        }
    
        partial void CustomInit();
    
        [JsonProperty(PropertyName = "resourceName")]
        public string ResourceName { get; set; }
    
    }

    And the Exception class is modeled as:

    public partial class ErrorModelException : HttpRestException<ErrorModel>
    {
        public ErrorModelException()
        {
        }
    
        public ErrorModelException(string message)
            : this(message, null)
        {
        }
    
        public ErrorModelException(string message, System.Exception innerException)
            : base(message, innerException)
        {
        }
    
        public string Description => Body.Description;
    
        public string SomeBaseProp => Body.SomeBaseProp;
    
        public string WhatWentWrong => Body.WhatWentWrong;
    
    }

    The generic type HttpRestException<T> indicates the error model it wraps, while the public getter methods return all the properties of wrapped model. This is a clean way of handling polymorphism of error models, deserialization and setting custom exception messages.

    And the Operation would be designed as:

    if ((int)_statusCode != 200)
    {
        try
        {
            switch ((int)_statusCode)
            {
                case 404:
                    await this.Handle404ErrorResponse(_httpRequest, _httpResponse);
                    break;
                default:
                    await this.HandleDefaultErrorResponse(_httpRequest, _httpResponse, (int)_statusCode);
                    break;
            }
        }
        catch (HttpRestException ex)
        {
            if (_shouldTrace)
            {
                ServiceClientTracing.Error(_invocationId, ex);
            }
            throw ex;
        }
    }

    where Handle404ErrorResponse would look like:

    private async Task Handle404ErrorResponse(HttpRequestMessage _httpRequest, HttpResponseMessage _httpResponse)
        => await HandleErrorResponse<NotFoundErrorException, NotFoundError>(_httpRequest, _httpResponse, string.Format("Operation failed, returned status code '{0}'", 404));
    
    private async Task HandleErrorResponse<T, V>(HttpRequestMessage _httpRequest, HttpResponseMessage _httpResponse, string errorMessage) where T : System.Exception, IHttpRestException<V>
    {
        string _responseContent = null;
        _httpResponse.Content = new StringContent("{\n\r \"resourceName\":\"MyResource\"\r\n, \"SomeBaseProp\":\"GreatBaseProp\" }");
        T ex = System.Activator.CreateInstance(typeof(T), new string[] { errorMessage }) as T;
    
        if (_httpResponse.Content != null) 
        {
            try
            {
                _responseContent = await _httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
                ex.SetErrorModel(Microsoft.Rest.Serialization.SafeJsonConvert.DeserializeObject<V>(_responseContent, Client.DeserializationSettings));
            }
            catch (JsonException)
            {
                // Ignore the exception
            }
        }
        
        ex.Request = new HttpRequestMessageWrapper(_httpRequest, null);
        ex.Response = new HttpResponseMessageWrapper(_httpResponse, _responseContent);
        _httpRequest.Dispose();
        if (_httpResponse != null)
        {
            _httpResponse.Dispose();
        }
        throw ex;
    }

    where IHttpRestException<V> is defined as:

    interface IHttpRestException<V>
    {
        HttpRequestMessageWrapper Request { get; set; }
        
        HttpResponseMessageWrapper Response { get; set; }
    
        void SetErrorModel(V model);
    }

    Note: HttpRestException is a generic base case for all exception classes as discussed in the section below.

  3. Allowing Exception classes to inherit from custom base class implementations. This enables customizations that can be shared between all exceptions thrown by service(s). The custom base class should ideally inherit from HttpRestException<T> or implements the interface IHttpRequestException<V> along with constructors that sets these properties and an empty constructor for serialization purposes. This is a contract that custom base classes should adhere to in order to ensure AutoRest generated exception classes compile successfully. The HttpRestException base class is designed as below:

    public abstract class HttpRestException<V> : RestException, IHttpRestException<V>
    {
        public HttpRestException()
        {
        }
    
        public HttpRestException(string message)
            : base(message, null)
        {
        }
    
        public HttpRestException(string message, System.Exception innerException)
        : base(message, innerException)
        {
        }
    
        protected V Body { get; set; }
    
        public void SetErrorModel(V model) => this.Body = model;
    
        public HttpRequestMessageWrapper Request { get; set; }
        
        public HttpResponseMessageWrapper Response { get; set; }
    
    }

    By default, all error models (exception classes) will inherit from HttpRestException<T>. Custom base classes for specific classes can be explicitly defined in the configuration file using directives as below. It is left to the author to ensure that the assembly containing the base class is referenced accordingly at compile time and the base class has the same structure and functionalities as HttpRestException<T>. In case an error model is not provided, we can mimic the current implementation, where body will be of CloudError type and the exception will be of CloudException type.

    An example directive to specify custom base class is as below:

    directive:
        from: code-model-v1
        where: $.modelTypes[?(@.name.raw=='NotFoundError')]
        transform: >
            const baseType = {
                "properties":{
    
                },
                "name":{
                    "fixed":false
                    "raw":"CustomBaseException"
                },
                "extensions":{
                    "x-ms-external":true
                }
            };
            $.baseModelType = JSON.stringify(baseType);
        reason: We want to model our own base classes

    The code generated will substitute HttpRestException<T> with CustomBaseException<T> in the code above. This can be extended to non-error models too.