kamilkisiela / apollo-angular

A fully-featured, production ready caching GraphQL client for Angular and every GraphQL server 🎁
https://apollo-angular.com
MIT License
1.5k stars 309 forks source link

Issues with .fetch not returning data on first query. #1812

Closed Destreyf closed 1 year ago

Destreyf commented 2 years ago

Describe the bug Calls to fetch never emit data while cache is empty.

I do not understand what's happening here, but it's become a blocker on my workflow.

When calling .watch().valueChanges I do get response data.

.fetch makes the network call, and If I log the output from my authentication interceptor, i see proper response data.

Furthermore, it appears that .fetch is populating the cache, if I make a second request, suddenly .fetch will return a result.

To Reproduce Steps to reproduce the behavior: I haven't been able to create a minimum reproduction yet, it seems to work fine on a basic install, I've reduced my configuration to be the same as a basic install and still run into problems.

Expected behavior I would hope that .fetch would return data on the first request.

Environment:

├── @angular/cli@14.2.1
├── @angular/core@14.2.0
├── @apollo/client@3.6.9
├── apollo-angular@4.0.1
├── graphql@16.6.0
└── typescript@4.7.4

Additional context I am still debugging this, but I am hoping someone else has run into this issue, or seen something, or maybe that I've just missed some big important section of the documentation.

This has brought this entire project to a complete standstill and I've been debugging it for over a week.

I am using GraphQL Code Generator with the following config:

overwrite: true
generates:
  libs/client/graphql/src/lib/graphql.generated.ts:
    documents: 'libs/client/graphql/src/lib/**/documents/**/*.graphql'
    schema:
      - http://localhost:8888/v1/graphql:
    config:
      addExplicitOverride: true
    plugins:
      - add:
          content:
            - '/* eslint-disable @typescript-eslint/no-empty-interface */'
            - '/* eslint-disable @typescript-eslint/no-explicit-any */'
            - '/* cSpell:disable */'
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-apollo-angular':
          addExplicitOverride: true
          pureMagicComment: true

my GraphQL Module looks like this:

// ...imports

const uri = '/v1/graphql'; // <-- add the URL of the GraphQL server here
export function createApollo(httpLink: HttpLink): ApolloClientOptions<any> {
  return {
    link: httpLink.create({ uri }),
    cache: new InMemoryCache(),
  };
}

@NgModule({
  imports: [ApolloModule, HttpClientModule],
  exports: [ApolloModule],
})
export class ClientGraphQLModule {
  static forRoot(): ModuleWithProviders<ClientGraphQLModule> {
    return {
      ngModule: ClientGraphQLModule,
      providers: [
        {
          provide: APOLLO_OPTIONS,
          useFactory: createApollo,
          deps: [HttpLink],
        },
      ],
    };
  }
}

These config items seem to work just fine in a bare repository, so I'm going to guess this is something in my repo, but I have no idea where to begin to look as to why it's not returning data.

Destreyf commented 2 years ago

Just as a follow up,

If I disable my auth interceptor the issue disappears, so this seems to be related to the way my auth service & auth interceptor is working, still digging further into this.

Destreyf commented 2 years ago

And I figured out what was happening.

This might make a good point in the documentation, but it could also use better documentation or at least a warning on the angular documentation.

If you call a service that emits an observable being used in your interceptor, but it never completes, this prevents the subscribe method from ever getting called on the down stream service. I'm not sure if this was introduced by angular later on as I've used this exact interceptor/user service on previous projects without issues.

If anyone runs into this, make sure that you emit a single value, an easy workaround is to use take(1) in the call chain before doing a switchMap, here's my interceptor:

/* eslint-disable @typescript-eslint/no-explicit-any */
// ...imports

const TOKEN_HEADER_KEY = 'Authorization';

const excludedURLs: Array<string> = [
  `auth/login`,
  `auth/logout`,
  `auth/refresh-token`,
];

@Injectable({ providedIn: 'root', deps: [AuthService] })
export class AuthInterceptor implements HttpInterceptor {
  constructor(private auth: AuthService) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    if (excludedURLs.some((url) => req.url.includes(url))) {
      // Do not intercept requests for login, logout, or refresh token
      return next.handle(req);
    }

    return this.auth.getToken().pipe(
      take(1), // MAKE SURE YOUR SERVICE WILL ONLY RETURN ONE VALUE.
      switchMap((token: JWT | null) => {
        if (!token || !token?.isValid) return next.handle(req);

        return next.handle(
          req.clone({
            headers: req.headers.set(
              TOKEN_HEADER_KEY,
              'Bearer ' + token.toString()
            ),
          })
        );
      })
    );
  }
}

export const AUTH_INTERCEPTOR_PROVIDER = [
  { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
];