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

Server Side Rendering Stalls when Apollo fails to return the responses from the server. #1698

Open superami-code opened 3 years ago

superami-code commented 3 years ago

I am trying to add Server Side Rendering (SSR) to my app. Angular-Apollo is working great in the regular app, but in server side rendering, it is simply never returning the results of a the queries and causing the render to stall. I have an HttpInterceptor for adding headers and I can see that the query results are being returned, but they are not processed any further by Apollo and my application render dies.

It might be important to note that I am using GraphQL Codegen to wrap the various queries, and that I'm running two queries, a page query and a country query, neither of which return.

My GraphQL Module

import {NgModule, InjectionToken} from '@angular/core';
import {
  TransferState,
  makeStateKey,
  BrowserTransferStateModule,
} from '@angular/platform-browser';
import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http';
// Apollo
import {APOLLO_OPTIONS} from 'apollo-angular';
import {HttpLink} from 'apollo-angular/http';
import {InMemoryCache} from '@apollo/client/core';
import { PageInterceptor } from './services/page.interceptor';
import { getEnv } from '@web-env/helper';

const APOLLO_CACHE = new InjectionToken<InMemoryCache>('apollo-cache');
const STATE_KEY = makeStateKey<any>('apollo.state');

@NgModule({
  imports: [
    HttpClientModule,
    BrowserTransferStateModule,
  ],
  providers: [
    {
      provide: APOLLO_CACHE,
      useValue: new InMemoryCache(),
    },
    {
      provide: APOLLO_OPTIONS,
      useFactory(
        httpLink: HttpLink,
        cache: InMemoryCache,
        transferState: TransferState,
      ) {
        const isBrowser = transferState.hasKey<any>(STATE_KEY);

        if (isBrowser) {
          const state = transferState.get<any>(STATE_KEY, null);
          cache.restore(state);
        } else {
          transferState.onSerialize(STATE_KEY, () => {
            return cache.extract();
          });
        }

        const env = getEnv();
        return {
          link: httpLink.create({uri: env.graphql}),
          cache,
          //connectToDevTools: !env.production,
          ssrMode: !isBrowser,
        };
      },
      deps: [HttpLink, APOLLO_CACHE, TransferState],
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: PageInterceptor,
      multi: true,
    },
  ],
})
export class GraphQLModule {}

My HttpInterceptor

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, throwError } from 'rxjs';
import { catchError, switchMap, tap } from "rxjs/operators";
import { PageService } from "./page.service";

@Injectable({
  providedIn: 'root'
})
export class PageInterceptor implements HttpInterceptor {
  constructor(
    private pageService: PageService
  ) {}

  intercept( req: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> {
    const id = new Date().getTime();
    this.pageService.setHttpActive(id);
    return this.pageService.headers.pipe(
      switchMap( setHeaders => next.handle(req.clone({setHeaders})) ),
      tap( () => this.pageService.clearHttpActive(id) ),
      tap( console.log ),
      catchError( err => {
        this.pageService.clearHttpActive(id);
        return throwError(err);
      })
    );
  }
}

Here is the CountryGQL Query Generated by GraphQL Codegen

export const CountryDocument = gql`
    query country {
  country: clientCountry {
    location
    country
    eu
    shop
  }
}
    `;
  @Injectable({
    providedIn: 'root'
  })
  export class CountryGQL extends Apollo.Query<CountryQuery, CountryQueryVariables> {
    document = CountryDocument;

    constructor(apollo: Apollo.Apollo) {
      super(apollo);
    }
  }
β”œβ”€β”€ @angular-devkit/build-angular@12.1.3
β”œβ”€β”€ @angular/cli@12.1.3 
β”œβ”€β”€ @angular/core@12.1.3 
β”œβ”€β”€ @apollo/client@3.3.21 
β”œβ”€β”€ apollo-angular@2.6.0 
β”œβ”€β”€ codelyzer@6.0.2
β”œβ”€β”€ graphql@15.5.1 
└── typescript@4.2.4 

Additional context

Unfortunately I am not seeing any error messages in the command line. I've tried using the debugger, but I haven't gotten anywhere with that either.

Again the code base works great in the live server, and production builds, but the SSR server is just stalling.

yharaskrik commented 2 years ago

I believe I am running into this one as well. Works fine if using ng serve, some additional info though, it only hangs on cold page loads. For example:

  1. spin up ng serve-ssr
  2. load page -> hangs (forever)
  3. refresh browser page
  4. Page will load this time and any subsequent times
  5. Can confirm that when I remove the gql from being used in universal the page loads on cold start no problems

Having optimization and enableProdMode on or off does not seem to make a difference.

We are rendering our Angular universal apps in an AWS Lambda function so depending on traffic the lambda function may have spun down and will cold start again thus causing the page to hang.

yharaskrik commented 2 years ago

Found the issue, we were using a subscription in a couple places and assumed that ssrMode: true would handle it but we were only providing an http link (no link for subscriptions) so something was just hanging. What worked for us was to do something along the lines of this:

const ws = isBrowser ? createClient() : () => void // no-op function in the case of the server
    const options = {
        provide: AURORA_NAMED_CLIENT_OPTIONS,
        useFactory: (httpLink, platformId, transferState) => ({ ssrMode: isPlatformServer(platformId), link: split(({ query }) => {
                const definition = getMainDefinition(query);
                return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
            }, ws, http)}),
        deps: [HttpLink, PLATFORM_ID, [new Optional(), TransferState]],
    }
pascalbayer commented 7 months ago

Having the same issue now with @angular/* as soon as Apollo is injected anywhere in the application, it never gets stable on the client within 10000ms. This is reproducible with an application setup from scratch (Angular 17.1.3), providing APOLLO_OPTIONS and e.g. injecting Apollo in the AppComponent.

neistow commented 6 months ago

Same issue with SSR. Hydration takes ~10 secs after the initial page load if Apollo is injected in any component that is rendered

PowerKiKi commented 6 months ago

This issue lacks a minimal (no codegen) reproduction case with latest Angular, using application builder, to be properly investigated.

neistow commented 6 months ago

@PowerKiKi I've made a repo with reproducible example on the latest angular version

The results are the following for the browser. ApplicationRef.isStable took ~10sec. image

For the server: image

neistow commented 6 months ago

I've managed to find out the issue.

...So the problem is in ApolloClient from @apollo/client/core. There's an option in the client called connectToDevTools, which triggers the devtools connect that uses setTimeout function for 10000ms.

I've removed the hydration stall by setting connectToDevTools to false. However, despite the claim that this option value is false in production mode the issue is reproducible when calling ng build && node dist/app-name/server/server.mjs

Not sure if this issue is fixable from apollo-angular side

yharaskrik commented 5 months ago

I've managed to find out the issue.

...So the problem is in ApolloClient from @apollo/client/core. There's an option in the client called connectToDevTools, which triggers the devtools connect that uses setTimeout function for 10000ms.

I've removed the hydration stall by setting connectToDevTools to false. However, despite the claim that this option value is false in production mode the issue is reproducible when calling ng build && node dist/app-name/server/server.mjs

Not sure if this issue is fixable from apollo-angular side

Interesting, maybe I have another issue going on because I get the hydration (stability) stalling even when connectToDevTools is disabled.

0ba100 commented 5 months ago

I've managed to find out the issue.

...So the problem is in ApolloClient from @apollo/client/core. There's an option in the client called connectToDevTools, which triggers the devtools connect that uses setTimeout function for 10000ms.

I've removed the hydration stall by setting connectToDevTools to false. However, despite the claim that this option value is false in production mode the issue is reproducible when calling ng build && node dist/app-name/server/server.mjs

Not sure if this issue is fixable from apollo-angular side

This fixed my issue!