Apollo-link: How to handle async requests?

Created on 9 Oct 2017  ·  3Comments  ·  Source: apollographql/apollo-link

Hi guys, excited for the upcoming release of Apollo Client 2.0! I'm trying to migrate over to the beta, and am having something difficulty setting up my auth middleware. My auth token is stored in my RN app via AsyncStorage, which (you guessed it), is asynchronous. Previously this wasn't an issue, as this was the middleware code I was using to inject the auth token into the header:

const authMiddleware = {
    async applyMiddleware(req, next) {
        if (!req.options.headers)
            // Create the header object if needed.
            req.options.headers = {};

        // Inject the auth header if it exists
        const authExists = await Storage.authToken.exists();

        if (authExists) {
            const token = await Storage.authToken.get();
            req.options.headers['Authorization'] = `Bearer ${token}`;
        }

        next();
    }
};

Now I've switched over to the new Link implementation, as documented here:

const authLink = new ApolloLink(async (operation, forward) => {
    const authExists = await Storage.authToken.exists();

    if (authExists) {
        const token = await Storage.authToken.get();

        operation.setContext({
            headers: {
                Authorization: `Bearer ${token}`
            }
        });
    }

    return forward(operation);
});

The only problem seems to be that the new library doesn't seem to like the async keyword. When I remove it and comment out all but the return statement, I get (as one would expect) a 403 error from my server. However, with the keyword added I get:

Network error: undefined is not a function (near '..._this.inFlightRequestObservables[key].subscribe...')

I imagine this has to do with the fact the request object is returning a promise instead of its expected value. Is there any approach that could remedy this?

Intended outcome:

The app should apply middleware despite the use of promises.

Actual outcome:

The above error is thrown if the async keyword is present.. even without it.. you'd still need to return the forward() call inside a then() method, which wouldn't work.

How to reproduce the issue:

I believe adding async to the request function of any ApolloLink constructor call would do it.

Most helpful comment

All 3 comments

So with enough tinkering I was able to actually come up with a fix myself:

const authLink = new ApolloLink((operation, forward) => {
    return new Observable(observable => {
        let sub = null;

        Storage.authToken.exists().then(exists => {
            if (exists) {
                Storage.authToken.get().then(token => {
                    operation.setContext({
                        headers: {
                            Authorization: `Bearer ${token}`
                        }
                    });

                    sub = forward(operation).subscribe(observable);
                });
            } else {
                sub = forward(operation).subscribe(observable);
            }
        });

        return () => (sub ? sub.unsubscribe() : null);
    });
});

That said, I'm very new to this library so I'm hoping my implementation doesn't open the door to any vulnerabilities. I think that because sub is only being referenced and not copied, the null value will resolve to the proper subscription once the Promise resolves. Anyway, closing for now. Hopefully this code helps someone.

I got the same error however without async on the constructor, but async on the Observable's constructor. You can replicate it as follows:

new ApolloLink(() => {
  return new Observable(async observer => {
    await new Promise((resolve, reject) => setTimeout(resolve, 1000))

    observer.next({data: {}})
    observer.complete()
  })
})

Or the most minimal step to replicate:

new ApolloLink(() => {
  return new Observable(() => {
    return new Promise()
  })
})

It's very odd as if I unroll the async/await as follows:

new ApolloLink(() => {
  return new Observable(observer => {
    const timeoutPromise = new Promise((resolve, reject) => setTimeout(resolve, 1000))

    timeoutPromise.then(() => {
      observer.next({data: {}})
      observer.complete()
    })
  })
})

Then all is well.

It seems that the Observable library does not like AsyncFunctions (which are essentially the same as a returned Promise)? I would not have guessed this as it shouldn't matter what the constructor returns as long as observer.{next/complete/error/etc} are called within the constructor accordingly.

Let me know if you require any additional information. Happy to open a separate issue to track this as well.

Thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

valerybugakov picture valerybugakov  ·  5Comments

NicholasLYang picture NicholasLYang  ·  4Comments

ignivalancy picture ignivalancy  ·  5Comments

j3ddesign picture j3ddesign  ·  3Comments

lobosan picture lobosan  ·  3Comments