angular
angular copied to clipboard
Allow caching HTTP requests when urls differ between server and client
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: https://github.com/angular/angular/issues/50117#issuecomment-1602675046 https://github.com/angular/angular/issues/50117#issuecomment-1648612777 https://github.com/angular/angular/issues/50117#issuecomment-1732133906 https://github.com/angular/angular/issues/50117#issuecomment-1792077410 https://github.com/angular/universal/issues/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 usingwithNoHttpTransferCache()
, - Provide
transferCacheInterceptorFn
asHTTP_ROOT_INTERCEPTOR_FNS
only whenwithNoHttpTransferCache()
is not used, - Remove the current error thrown when both
withNoHttpTransferCache()
andwithHttpTransferCacheOptions()
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.
While it is a very reasonable desire to use local network when doing SSR vs public API endpoints, there are probably existing alternatives.
Couldn't this be solved by a reverse proxy within the same “network” ? (Like if you're using docker).
I'm not a network expert so I don't know if it would be feasible or if it would cover every use case.
But anyway, I don't think that the frontend framework should put that much constraint on how a private network that is managed by dedicated teams in big companies should be set up.
I edited the first message to suggest a second approach that would allow more customization for the end user without adding more options to the existing API by exposing the transferCacheInterceptorFn
in the public API
I too have a very similar use case where our API domain differs server side and client side. Our Angular application, along with the other applications, are build within Docker containers - most of which are in the same network.
When a server side request is made, we reference use a HTTP interceptor to replace the API domain with the container name so that we directly communicate with the API container. Client side uses the public facing domain.
The main challenges come when we're developing locally and I may have my main webserver running on localhost:8010
for example which does not resolve within the Docker network (only resolves on my host machine). Similarly, the client side would need to use web-api:4040
for example which does not resolve client side (on my host machine).
The requests are going to the same API with the same response, the URLs are the same (expect domain), however, client side rehydration always occurs due to the domain change.
It would be brilliant if we were able to configure provideClientHydration
in such a way so that the caching would understand that the 2 domains are "the same".
@BenStudee, the cache only prevents the request from being sent to the backend. Hydration happens either way.
Also, couldn't your dev issues be solved by the CLI proxy ?
@BenStudee, the cache only prevents the request from being sent to the backend. Hydration happens either way.
Also, couldn't your dev issues be solved by the CLI proxy ?
Thanks for your response @JeanMeche. Perhaps I'm using the wrong terminology - I am referring to the client executing the HTTP requests when the server has already hydrated the components.
As for CLI proxy - I don't believe so, but if you have an idea in mind and care to share then I am more than happy to listen.
Having the ability to customise how the "cache key" is generated would be enough to overcome the challenge I am facing - at least I believe so.
While it is a very reasonable desire to use local network when doing SSR vs public API endpoints, there are probably existing alternatives.
Couldn't this be solved by a reverse proxy within the same “network” ? (Like if you're using docker).
In some conditions this couldn’t be done (eg your DevOps team can’t do this, or uses k8s with containerd etc, where configs looks different). So giving option to customize cacheKey is the best solution here.
This is exact use case that I have. I really hope PR will be merged.
Hi @AndrewKushnir, do you have any thoughts on this issue and the related PR please ?
It looks like a fairly requested feature based on the reactions and comments and it would be awesome to have some feedback from the angular team given that there is already a PR opened to try to address it.
@jsaguet we had a conversation about this with the team last week and the proposal is to create a new token that would map an internal origin to a public one. The token would only be used on the server - we can add some logic to throw an error (in dev mode) if a token is present on the client.
Here is a quick example of how a token can be provided in the app.config.server.ts
:
{
provide: HTTP_TRANSFER_CACHE_ORIGIN_MAP,
useValue: {
'http://internal-domain.com:1234': 'https://external-domain:443',
// ...
}
}
In this case, the HttpTransferCache
logic would inject the token while running on the server, check if there is an entry for a given URL and use a public origin info to calculate a cache key. While running on the client, the public version would be used for a cache key, so we can find the necessary data.
Please let us know if that would work for your use-cases.
If you are interested in contributing a PR for this change (since you've created PR #53815, which uses a different approach) - please let us know, we'll be happy to review and provide the necessary feedback.
@AndrewKushnir Thanks for your reply! I think this approach would definitely solve this issue.
We are currently using a map between internal and public domains to implement our own custom transfer cache so this makes sense. It would be easier to use than carefully placing the interceptor in the chain.
I will work on a PR.
This issue has been automatically locked due to inactivity. Please file a new issue if you are encountering a similar or related problem.
Read more about our automatic conversation locking policy.
This action has been performed automatically by a bot.