Skip to content

Allow caching HTTP requests when urls differ between server and client #53702

@jsaguet

Description

@jsaguet

Which @angular/* package(s) are relevant/related to the feature request?

common, platform-browser

Description

Following #50117, additional configuration was added to the HTTP transfer cache.
One use-case was mentioned in several replies and other issues about the lack of customization of the cache key but it was not handled in the associated PR so I'm opening a new issue, dedicated to this use case.

When using SSR, it is common to use different urls on the server and the browser to access the same APIs.
It could be only the domain, the path or even the http scheme.
The main reason is to reduce network latency by accessing resources directly from a private network.

With the current HTTP transfer cache implementation, the cache key is computed from the HttpRequest with the makeCacheKey function.

This function uses the request url among other things so when the url is different on the server and on the browser, the cached response will never be reused from the transfer cache during hydration.

Related comments / issues:
#50117 (comment)
#50117 (comment)
#50117 (comment)
#50117 (comment)
angular/universal#1934

First proposed solution: adding more options to HttpRequest and provideClientHydration()

To override the cache key at the request level, we could provide a custom cache key as a request option.

const customCacheKey = makeCustomCacheKey(url, params);
this.http.get(url, { transferCache: { cacheKey: customCacheKey } });

To globally customize the cache key, we could either use an interceptor (Ability to override transferCache property when cloning the request is needed too)

function customCacheKeyInterceptor = (req, next) => {
    const newRequest = req.clone({ 
        transferCache: { 
            cacheKey: customCacheKey(req) // Custom logic possibly using DI to compute the cache key
            ...req.transferCache,
        }
    });
    return next(newRequest);
}

Or we could add a "custom cache key function" option to provideClientHydration():

provideClientHydration({
    customCacheKeyFn: myCustomCacheKeyFn
});

I think that providing a custom cache key function could simplify the usage of and totally replace the existing options includeHeaders, filter and includePostRequests. (but that would be breaking)
These options would probably need to be mutually exclusive because they could overlap.

Another useful thing would be to have access to the current makeCacheKey function so we could simply override some parts of the http request to customize the cache key:

import { makeCacheKey } from '@angular/common/http';

function customCacheKey(request: HttpRequest) {
    // If only the url is different between server and browser, we can only override this part
    const overridenRequest = request.clone({ url: 'urlToUseInCacheKey' });
    return makeCacheKey(overridenRequest);
}

function customCacheKeyInterceptor = (req, next) => {
    const newRequest = req.clone({ 
        transferCache: { 
            cacheKey: customCacheKey(request)
            ...req.transferCache,
        }
    });
    return next(newRequest);
}

Second proposed solution: expose transferCacheInterceptorFn and let developers use it where needed

Another way to solve this (closest to the original behavior with TransferHttpCacheModule) would be to expose the transferCacheInterceptorFn in the public API.

We could place this interceptor exactly where we need it in the interceptors chain to control what the request url looks like when cached.

Caching would be enabled by using the interceptor function in provideHttpClient() and calling provideClientHydration().

provideHttpClient(
    withInterceptors([
        authInterceptorFn, // Add Auth header
        transferCacheInterceptorFn, // Cache requests when they are still undifferentiated between browser & server
        overrideUrlInterceptorFn, // Custom logic to override the url on server, specific to the user's use case
])),
provideClientHydration(
    withNoHttpTransferCache(), // disable injecting transferCacheInterceptorFn as the last interceptor
    withHttpTransferCacheOptions({ includePostRequests: true }) // optionally provide global options for transferCacheInterceptorFn 
),

Because the interceptor would still rely on CACHE_OPTIONS which is only provided by provideClientHydration(), we could make sure that the interceptor is used with provideClientHydration() and print a warning/error otherwise.

This approach would require minimal changes to the current implementation without adding any breaking change:

  • Always provide CACHE_OPTIONS even when using withNoHttpTransferCache(),
  • Provide transferCacheInterceptorFn as HTTP_ROOT_INTERCEPTOR_FNS only when withNoHttpTransferCache() is not used,
  • Remove the current error thrown when both withNoHttpTransferCache() and withHttpTransferCacheOptions() are used together.

I believe that this second approach is the best one because it keeps a simple API for simple cases while giving the most flexibility for more advanced use cases. And all of that without any breaking change.

I opened a PR to show how little changes would be needed to implement it.

Alternatives considered

The only alternative is to disable transfer cache from provideClientHydration and rely on a custom caching logic based on an interceptor (like the old TransferHttpCacheModule from @angular/universal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3An issue that is relevant to core functions, but does not impede progress. Important, but not urgentarea: commonIssues related to APIs in the @angular/common package

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions