How to configure GraphQL request with interceptors on the example of JWT authentication

  1. GraphQL request – minimalistic and simple graphql client that can be conveniently combined with any state manager.
  2. Interceptors – сonvenient methods for modifying requests and responses that are widely used by http clients such as axios.

  3. As part of this tutorial, we will consider a configuration option for a GraphQL request using the example of forwarding a header with an access token to a request and intercepting a 401 response error to refresh this token.

Link to documentation: https://www.npmjs.com/package/graphql-request


So let’s get started.

Step 1. Installing the package

yarn add graphql-request graphql

Step 2. Create a request context class

export class GQLContext 

    private client: GraphQLClient
    private snapshot: RequestSnapshot;
    private readonly requestInterceptor = new RequestStrategy();
    private readonly responseInterceptor = new ResponseStrategy();

    public req: GQLRequest;
    public res: GQLResponse;
    public isRepeated = false;

    constructor(client: GraphQLClient) 
        this.client = client
    

    async setRequest(req: GQLRequest) 
        this.req = req
        await this.requestInterceptor.handle(this)
    

    async setResponse(res: GQLResponse) 
        this.res = res
        await this.responseInterceptor.handle(this)
    

    async sendRequest(): Promise<GQLResponse> 
        if (!this.snapshot) 
            this.createSnapshot()
        
        const res = await this.client.rawRequest.apply(this.client, new NativeRequestAdapter(this)) as GQLResponse
        await this.setResponse(res)

        return this.res
    

    async redo(): Promise<GQLResponse> 
        await this.snapshot.restore()
        this.isRepeated = true
        return await this.sendRequest()
    


    createSnapshot() 
        this.snapshot = new RequestSnapshot(this)
    

This class will contain data about the request, response (upon receipt), as well as store the reference to the GQL client itself.
To set the request context, two methods are used: setRequest and setResponse. Each of them applies an appropriate strategy of using interceptors, each of which we will discuss below.

Let’s take a look at the snapshot structure:

export class RequestSnapshot 

    instance: GQLContext;
    init: GQLRequest;

    constructor(ctx: GQLContext) 
        this.instance = ctx
        this.init = ctx.req
    

    async restore() 
        await this.instance.setRequest(this.init)
    

The snapshot receives a reference to the execution context, and also saves the state of the original request for subsequent restoration (if necessary) using the restore method

The sendRequest method will serve as a wrapper for gql-request, making it possible to create a snapshot of the original request using the createSnapshot method

NativeRequestAdapter is an adapter that serves to bring our context object to the form that the native gql-request can work with:

export function NativeRequestAdapter (ctx: GQLContext)
    return Array.of(ctx.req.type, ctx.req.variables, ctx.req.headers)

The redo method is used to repeat the original request and consists of three basic steps:
1) Reconstructing the context of the original request
2) Set the flag indicating that the request is repeated
3) Repeat the original request

Step 3. Registering our own error type

export class GraphQLError extends Error 
    code: number;

    constructor(message: string, code: number) 
        super(message)
        this.code = code
    

In this case, we are simply extending the structure of a native JS error by adding a response code there.

Step 4. Writing an abstraction for an interceptor

For writing an abstraction of an interceptor, the “Chain of Responsibility (СoR)” behavioral programming pattern is perfect. This pattern allows you to sequentially transfer objects along a chain of handlers, each of which independently decides how exactly the received object should be processed (in our case, the object will be our request context), as well as whether it is worth passing it further along the chain.
So let’s take a closer look at this concept:

export type GQLRequest = 
    type: string;
    variables?: any;
    headers?: Record<string, string>

export type GQLResponse = 
    data: any
    extensions?: any
    headers: Headers,
    status: number
    errors?: any[];



interface Interceptor 
    setNext(interceptor: Interceptor): Interceptor;

    intercept(type: GQLContext): Promise<GQLContext>;


export abstract class AbstractInterceptor implements Interceptor 

    private nextHandler: Interceptor;

    public setNext(interceptor: Interceptor): Interceptor 
        this.nextHandler = interceptor
        return interceptor
    

    public async intercept(ctx: GQLContext) 
        if (this.nextHandler) return await this.nextHandler.intercept(ctx)

        return ctx
    


You can see two methods here:

  1. setNext – designed to set the next interceptor in the chain, a reference to which we will store in the nextHandler property
  2. intercept – the parent method is intended to transfer control to the next handler. This method will be used by child classes if necessary


Step 5. Request Interceptor Implementation

export class AuthInterceptor extends AbstractInterceptor{
    intercept(ctx: GQLContext): Promise<GQLContext> {
        if (typeof window !== 'undefined') 

            const token = window.localStorage.getItem('token')
            if (!!token && token !== 'undefined') 
                ctx.req.headers = 
                ...ctx.req.headers, 
                Authorization: `Bearer $token`
                
            
        
        return super.intercept(ctx) 
    }

}

This interceptor gets the access token from localStorage and adds a header with the token to the request context

Step 6. Response Interceptor Implementation

Here we will implement interception of 401 errors and, if received, we will make a request to refresh the token and repeat the original request.

export const REFRESH_TOKEN = gql`
    query refreshToken 
        refreshToken
            access_token
        
    
`

export class HandleRefreshToken extends AbstractInterceptor {
    async intercept(ctx: GQLContext): Promise<GQLContext> 

        if ( !('errors' in ctx.res)) return await super.intercept(ctx)

        const exception = ctx.res.errors[0]?.extensions?.exception

        if (!exception) return await super.intercept(ctx)

        const Error = new GraphQLError(exception.message, exception.status)
        if (Error.code === 401 && !ctx.isRepeated && typeof window !== 'undefined') 
            try 
                await ctx.setRequest(type: REFRESH_TOKEN)
                const res = await ctx.sendRequest()
                localStorage.setItem('token', res.refreshToken.access_token)
                await ctx.redo()

                return await super.intercept(ctx)
             catch (e) 
                throw Error
            
        
        throw Error
    
}
  1. First, we check if there are any errors in the request. If not, then we transfer control to the next handler. If so, we are trying to get the exсeption.

  2. From the exсeption we get the response status and the error code

  3. Сheck if the error code is 401, then we make a request to refresh the token, and write a new access token in localStorage

  4. Then we repeat the original request using the redo method, which we discussed earlier.

  5. If this operation is successful, then we pass the request to the next handler. Otherwise, throw an error and stop processing.

Step 7. Writing a strategy abstraction

export abstract class InterceptStrategy 

    protected makeChain(collection: AbstractInterceptor[]) 
        collection.forEach((handler, index) => collection[index + 1] && handler.setNext(collection[index + 1]))
    

    abstract handle(ctx: GQLContext): any;

Strategy abstraction is represented by two methods:

  1. makeChain – a helper that allows you to conveniently assemble a chain of handlers from an array
  2. handle – a method that implements the main logic of the processing strategy, we will describe it in the implementations

Step 8. Implementing request and response interception strategies

export class RequestStrategy extends InterceptStrategy

    async handle(ctx: GQLContext): Promise<GQLContext> 
        const handlersOrder: AbstractInterceptor[] = [
            new AuthInterceptor(),
        ]
        this.makeChain(handlersOrder)

        return await handlersOrder[0].intercept(ctx)
    

export class ResponseStrategy extends InterceptStrategy

    async handle(ctx: GQLContext): Promise<GQLResponse['data']> 
        const handlersOrder: AbstractInterceptor[] = [
            new HandleRefreshToken(),
            new RetrieveDataInterceptor(),
        ]
        this.makeChain(handlersOrder)

        return await handlersOrder[0].intercept(ctx)
    

As we can see, both strategies look absolutely identical in structure. Notice the handle method, which:

  1. Determines the order of invocation of handlers
  2. Creates a chain of them using the parent makeChain method
  3. And starts the processing


Step 9. Putting it all together.

const request = async function (this: GraphQLClient, type: string, variables: any, headers = ): Promise<any> 

    const ctx = new GQLContext(this)
    await ctx.setRequest(type, variables, headers)
    try 
        await ctx.sendRequest()
     catch (e) 
        await ctx.setResponse(e.response)
    

    return ctx.res


GraphQLClient.prototype.request = request

export const client = new GraphQLClient('http://localhost:4000/graphql', 
    credentials: 'include',
)
  1. Override the base request method supplied by the package.
  2. Inside our method, create a context
  3. Set the initial parameters of the request
  4. Send a request and set a response
  5. Returning response data
  6. Export the created client

Thanks for reading. I would be glad to receive your feedback.
Link to repository: https://github.com/IAlexanderI1994/gql-request-article


Source link