3

I am trying to implement built-in TransferHttpCacheModule in order to de-duplicate requests. I am using this interceptor in my app:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthenticationService);
    const url = `${this.request ? this.request.protocol + '://' + this.request.get('host') : ''}${environment.baseBackendUrl}${req.url}`

    let headers = new HttpHeaders();

    if (this.request) {
      // Server side: forward the cookies
      const cookies = this.request.cookies;
      const cookiesArray = [];
      for (const name in cookies) {
        if (cookies.hasOwnProperty(name)) {
          cookiesArray.push(`${name}=${cookies[name]}`);
        }
      }
      headers = headers.append('Cookie', cookiesArray.join('; '));
    }

    headers = headers.append('Content-Type', 'application/json');

    const finalReq: HttpRequest<any> = req.clone({ url, headers });
    ...

It enables relative URLs for client side and full URLs for server side since the server is not aware of its own URL.

The problem is that TransferHttpCacheModule uses a key based on the method, the URL and the parameters, and the server URLs don't match with the client URLs.

Is there any way to force the TransferHttpCacheInterceptor to execute before my own interceptor? I want to avoid forcing full URLs on client side.

Guerric P
  • 30,447
  • 6
  • 48
  • 86

3 Answers3

6

You can place your interceptor inside its own module:

@NgModule({
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: MyOwnInterceptor, multi: true }
  ]
})
export class MyOwnInterceptorModule {}

You can then place this module below the import of the TransferHttpCacheModule inside your AppModule:

@NgModule({
  imports: [
    // ...
    TransferHttpCacheModule,
    MyOwnInterceptorModule
  ],
  // ...
})
export class AppModule {}

This way your interceptor will be applied after the TransferHttpCacheInterceptor. It feels weird though, because as far as I know, an import is first in line, and then the providers. This way you can override providers from imports. Are you sure you don't want it the other way around?

Poul Kruijt
  • 69,713
  • 12
  • 145
  • 149
  • that works indeed, I'll still wait until the end of the bounty just in case – Guerric P Nov 27 '18 at 21:02
  • @YoukouleleY I understand :) glad I could help – Poul Kruijt Nov 27 '18 at 21:17
  • 1
    Also do you know if this is by design or just a workaround? – Guerric P Nov 28 '18 at 16:39
  • @YoukouleleY It is by design that the import order, and provider order inside an NgModule is important. But can you clarify if you want your own interceptor to be run before or after the `TransferHttpCacheInterceptor` – Poul Kruijt Nov 28 '18 at 19:03
  • As i wrote in the question, I want my own interceptor to run after the `TransferHttpCacheInterceptor` because when it runs before, the URL changes and so the cached URL doesn't match the client URL – Guerric P Nov 28 '18 at 20:06
  • It feels like a bug to me, that a provider is placed before an imported module provider when `multi` is used. But I have to dig into the source to see if that's the case – Poul Kruijt Nov 28 '18 at 21:09
1

I have had the same problem for Angular universal support in angularspree

I followed these methods:

=> Create a TransferStateService, which exposes functions to set and get cache data.

import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { isPlatformBrowser } from '@angular/common';

/**
 * Keep caches (makeStateKey) into it in each `setCache` function call
 * @type {any[]}
 */
const transferStateCache: String[] = [];

@Injectable()
export class TransferStateService {
  constructor(private transferState: TransferState,
    @Inject(PLATFORM_ID) private platformId: Object,
    // @Inject(APP_ID) private _appId: string
  ) {
  }

  /**
   * Set cache only when it's running on server
   * @param {string} key
   * @param data Data to store to cache
   */
  setCache(key: string, data: any) {
    if (!isPlatformBrowser(this.platformId)) {
      transferStateCache[key] = makeStateKey<any>(key);
      this.transferState.set(transferStateCache[key], data);
    }
  }


  /**
   * Returns stored cache only when it's running on browser
   * @param {string} key
   * @returns {any} cachedData
   */
  getCache(key: string): any {
    if (isPlatformBrowser(this.platformId)) {
      const cachedData: any = this.transferState['store'][key];
      /**
       * Delete the cache to request the data from network next time which is the
       * user's expected behavior
       */
      delete this.transferState['store'][key];
      return cachedData;
    }
  }
}

=> Create a TransferStateInterceptor to intercept request on server side platform.

import { tap } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpResponse
} from '@angular/common/http';
import { TransferStateService } from '../services/transfer-state.service';

@Injectable()
export class TransferStateInterceptor implements HttpInterceptor {
  constructor(private transferStateService: TransferStateService) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    /**
     * Skip this interceptor if the request method isn't GET.
     */
    if (req.method !== 'GET') {
      return next.handle(req);
    }

    const cachedResponse = this.transferStateService.getCache(req.url);
    if (cachedResponse) {
      // A cached response exists which means server set it before. Serve it instead of forwarding
      // the request to the next handler.
      return of(new HttpResponse<any>({ body: cachedResponse }));
    }

    /**
     * No cached response exists. Go to the network, and cache
     * the response when it arrives.
     */
    return next.handle(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          this.transferStateService.setCache(req.url, event.body);
        }
      })
    );
  }
}

=> Add that to the provider section in your module.

providers: [
  {provide: HTTP_INTERCEPTORS, useClass: TransferStateInterceptor, multi: true},
  TransferStateService,
]
pkrawat1
  • 671
  • 7
  • 18
1

I've had the same problem and solved it by removing the host in makeStateKey.

Your OwnHttpInterceptor

You can change this

const key: StateKey<string> = makeStateKey<string>(request.url);

to this

const key: StateKey<string> = makeStateKey<string>(request.url.split("/api").pop());
Marcel
  • 381
  • 1
  • 5
  • 19