How to use HTTP Transfer State when using relative URLs

2.2k Views Asked by At

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.

3

There are 3 best solutions below

6
On BEST ANSWER

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?

1
On

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,
]
0
On

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());