2019年8月31日 星期六

[Axios] Interceptor - Error handling


 Axios   Interceptor   Error handling   Typescript 



Introduction


Axios is the Promise based HTTP client, and is often used in my Vue.js projects.
This article shows how to integrate the custom error to handle non-2XX response and timeout request, so that we can show customized message for end-user, not the real error.

The concept and sample codes in based on Axios interceptor by Typescript.
However, they can be apply to other Http Client framework, and of course, write the code in Javascript.


Environment


axios 0.19.0
typescript 3.2.1



Implement

          
(Optional) Set default valid Http Status Codes

The default valid Http Status Codes are >= 200 and < 300.
That means the response with status 200~299 will NOT be rejected by Axios.
You can change the default value by setting axios.defaults.validateStatus,

For example, the 498 response will not be rejected by Axios with the following configuration:

import axios, { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios';

const instance = axios.create();

// Overwrite the ValidateStatus settings
instance.defaults.validateStatus = (status: number) => {
  return (status >= 200 && status < 300) || status == 498;
};

export default instance;



Error handling for rejected Http response

We can create custom error class with custom (and international!) error message for certain rejected response.

MyCustomError.ts


export default class MyCustomError extends Error {
  public url: string;
  public statusCode: number | undefined;
  constructor(message?: string, url?: string, status?: number) {
    super(message);
    this.name = 'MyCustomError';
    this.url = String(url);
    this.statusCode = status;
    this.stack = new Error().stack;
  }
  /**
   * Error msg for server side
   * @memberof HttpRequestError
   */
  public toServerString = (): string => {
    return `[${this.name}][${this.statusCode || '??'}][${this.url}] ${this.message}`;
  }
  /**
   * Error msg for end-user
   * @memberof HttpRequestError
   */
  public toString = (): string => {
    return i18n.tc('error.httpRequestError');
  }
}


Notice that we separate the client-side error message from the server-side message because we don’t want the end-user to get the error’s detail J


Error handling for rejected Http response

Now we can integrate the custom error into the Axios interceptor.

import axios, { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios';
import JL from '@/modules/jsnlogger';
import store from '@/store';
import { MyCustomError } from '../error-model';

const instance = axios.create();

instance.interceptors.response.use( (response: AxiosResponse<any>) => {
  return response;
}, async (error: any) => {
 
  const { config, response: { status } } = error;
  const originalRequest = config;
  const url = originalRequest.url;
 
// Error handling here ...
  // For example, use JsnLog to save server-side log on server side
  const httpErr = new HttpRequestError(error, url, status);
JL.error(httpErr.toServerString());
 
// Reject the error
  return Promise.reject(httpErr);
});

export default instance;



With the above Axios instance, we can easily to catch the custom error and show the custom client-side’s error message as following,

try {
    const url = 'api/xxxx';
    const res: any = await axios.get(url);
    return res.data;
} catch (err) {
    console.log(String(err)); // This will shows the custom client-side msg
}


For example, zh-TW:



en-US:





Error handling for Timeout request

Axios has default timeout as 0 (no timeout).
We can change the default timeout as following,

const instance = axios.create();
instance.defaults.timeout = 3000; // Million seconds


Or by interceptor…


instance.interceptors.request.use( (config: AxiosRequestConfig) => {
 
  config.timeout = 3000; // Million seconds
  return config;

}, (error) => {
  return Promise.reject(error);
});



We can create another custom error model: MyTimeoutError (just like MyCustomError), to handle the timeout error.
Then we can update the Axios interceoptor:

instance.interceptors.response.use( (response: AxiosResponse<any>) => {
  return response;
}, async (error: any) => {
  if (error.code === 'ECONNABORTED') { // Timeout error
    const timeoutErr = new MyTimeoutError(error, error.config.url);
    // Do something…
    return Promise.reject(timeoutErr);
  } else {
    // Handle other err with MyCustomError model ...
  } 
});



Source code





Conclusion

The interceptor allows us to handle and control corresponding error on different responses or scenarios.
So that we can focus on the presentation or business logics when sending a Http request.



Reference







2019年8月14日 星期三

[ASP.NET Core] Identity Server 4 – Custom Event Sink


 ASP.NET Core   Identity Server 4   Event Sink  


Introduction



Events represent higher level information about certain operations in IdentityServer. Events are structured data and include event IDs, success/failure information, categories and details. (From docs.identityserver.io)


We are going to create a custom Event Sink which can save successful sign-in information into memory cache.
Notice that the event is fired on Auth Server application.





docker-openldap 1.2.4 (OpenLDAP 2.4.47)
ASP.NET Core 2.2.203
IdentityServer4 2.4.0
Nordes/IdentityServer4.LdapExtension 2.1.8



Implement


The source code is on my Github.

Enable raising events

Events are not turned on by default, make sure enabling raising events are configured in the ConfigureServices method.

Startup.cs : ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
   services.AddMvc();

   #region Identity Server

   var builder = services.AddIdentityServer(options =>
   {
       options.Events.RaiseErrorEvents = true;
       options.Events.RaiseInformationEvents = true;
       options.Events.RaiseFailureEvents = true;
       options.Events.RaiseSuccessEvents = true;
   });
   #endregion
}


Since we will use Memory Cache and Session later when implementing the Custom Event Sink, we have to
1. Add IMemoryCache into IServiceCollection for DI
2. Add SessionMiddleware to enable session state for the application



Add IMemoryCache into IServiceCollection for DI

Add implementation of Microsoft.Extensions.Caching.Memory.IMemoryCache into IServiceCollection as following,

Startup.cs : ConfigureServices

 public void ConfigureServices (IServiceCollection services)
 {
        // … Skip

        services.AddMemoryCache();      
 }



Enable Session

Startup.cs: Configure

 public void Configure (IApplicationBuilder app, IHostingEnvironment env)
 {
        // … Skip

        app.UseSession();

        app.UseMvc ();
 }


Raise event(s)

Identity Server 4 uses the DI service: IEventService, to raise event(s) as below.
Notice that we cannot pass parameter while raising events, we save the given Token in Session so that we can get it later in an Event Sink class.

LdapController.cs

Here is the sample for raising the UserLoginSuccessEvent.

 [Route("api/[controller]")]
 [ApiController]
 public class LdapController : ControllerBase
    {
        private readonly ILdapUserStore userStore = null;
        private readonly IEventService events = null;
        private readonly IdentityServerTools tools = null;

        public LdapController(
            ILdapUserStore userStore,
            IEventService events,
            IdentityServerTools tools)
        {
            this.userStore = userStore;
            this.events = events;
            this.tools = tools;
        }

        [HttpPost("SignIn")]
        public async Task<IActionResult> SignIn([FromBody]LdapUser model)
        {
            // validate username/password against Ldap
            var user = this.userStore.ValidateCredentials(model.Username, model.Password);

            if (user != default(IAppUser))
            {
                // Response with authentication cookie
                await this.HttpContext.SignInAsync(user.SubjectId, user.Username);

                // Get the Access token
                var accessToken = await this.tools.IssueJwtAsync(lifetime: 3600, claims: new Claim[] { new Claim(JwtClaimTypes.Audience, model.ApiResource) });

                // Save Access token to current session
                this.HttpContext.Session.SetString("AccessToken", accessToken);

                // Write the Access token to response
                await this.HttpContext.Response.WriteAsync(accessToken);

                // Raise UserLoginSuccessEvent
                await this.events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username));

                return this.Ok();
            }
            else
            {
                return this.Unauthorized();
            }
        }
 }


Raising the event(s), such as UserLoginSuccessEvent, will trigger any Custom Event Sink that had already been added to IServiceCollection.
Next step we will create a Custom Event Sink.



Custom Event Sink

Create a new Event Sink class, "UserProfileCacheSink", which implement IdentityServer4.Services.IEventSink.


UserProfileCacheSink.cs

 public class UserProfileCacheSink : IEventSink
 {
        public async Task PersistAsync(Event evt)
        {
           
        }
    }


First inject the necessary services,

 public class UserProfileCacheSink : IEventSink
 {
        private IHttpContextAccessor httpContextAccessor = null;
        private readonly ICacheKeyFactory cacheKeyFactory = null;
        private readonly IMemoryCache cache = null;
        private readonly ILogger<UserProfileCacheSink> logger = null;
   
        public UserProfileCacheSink(IHttpContextAccessor httpContextAccessor, ICacheKeyFactory cacheKeyFactory, IMemoryCache cache, ILogger<UserProfileCacheSink> logger)
        {
            this.httpContextAccessor = httpContextAccessor;
            this.cacheKeyFactory = cacheKeyFactory;
            this.cache = cache;
            this.logger = logger;
        }
        
        public async Task PersistAsync(Event evt)
        {
           
        }
    }


Now we can implement the PersistAsync method, which will be fired when raising events.
The main logic of PersistAsync is to store the {Subject : JWT token} json string, for example { "jblin","xxxxxxxxxx" }, into Memory Cache.

 public async Task PersistAsync(Event evt)
 {
            if (evt.Id.Equals(EventIds.UserLoginSuccess))
            {
                if (evt.EventType == EventTypes.Success || evt.EventType == EventTypes.Information)
                {
                    var httpContext = this.httpContextAccessor.HttpContext;

                    try
                    {
                        if (this.httpContextAccessor.HttpContext.Session.IsAvailable)
                        {
                            var session = this.httpContextAccessor.HttpContext.Session;
                            var user = this.httpContextAccessor.HttpContext.User;
                            var subject = user.Claims.Where(x => x.Type == "sub").FirstOrDefault()?.Value;
                            var token = session.GetString("AccessToken");
                            string cacheKey = this.cacheKeyFactory.UserProfile(subject);
                            _ = await this.cache.GetOrCreateAsync<JObject>(cacheKey, async entry =>
                            {
                                entry.SlidingExpiration = TimeSpan.FromSeconds(600);
                                string jsonStr = $"{{\"{subject}\":\"{token}\"}}";
                                return JObject.Parse(jsonStr);
                            });

                            // Check if the cache exist
                            if (this.cache.TryGetValue<JObject>(cacheKey, out JObject tokenInfo))
                            {
                               Debug.WriteLine($"Cached: {tokenInfo.ToString()}");
                            }
                            
                        }
                    }
                    catch (Exception)
                    {
                    }
                }
                else
                {
                    this.logger.LogError($"{evt.Name} ({evt.Id}), Details: {evt.Message}");
                }
           }
 }


Last but very important!
We have to inject the Custom Event Sink, in other words, add the UserProfileCacheSink into IServiceCollection.
So that raising event(s) will also fires our Custom Event Sink.

Startup.cs:ConfigureServices                                                                              

 public void ConfigureServices (IServiceCollection services)
 {
         // … Skip

         #region Custom sinks
         services.AddScoped<IEventSink, UserProfileCacheSink>();
         #endregion
 }


Result: The information is stored in Memory Cache as following runtime log.




Source Code




Reference