Angular Http拦截器缓存返回不可变响应引发只读属性错误
Angular HTTP拦截器缓存响应时出现"genre_ids is read-only"错误
问题场景
我使用Angular的HTTP拦截器实现请求缓存逻辑:若请求已发起过,拦截器直接返回缓存的HttpResponse。
触发缓存的API请求方法
getSeriesWithGenres() { const series$ = this.getSeries() const genres$ = this.getGenres() return forkJoin([series$, genres$]).pipe( map(([series, genres]) => { return series.map((serie: Serie) => { serie.genre_ids = serie.genre_ids.map(genreId => { return genres.find((genre: Genre) => genre.id === genreId)?.name }) return serie }) }) ) }
发起HTTP请求的核心方法
getSeries() { return this.http .get<ApiResponse>( ` https://api.themoviedb.org/3/tv/popular?api_key=${environment.apiKey}&language=en-US ` ) .pipe(map((data: ApiResponse) => data.results as Serie[])) } getGenres() { return this.http .get( ` https://api.themoviedb.org/3/genre/tv/list?api_key=${environment.apiKey}&language=en-US ` ) .pipe(map((data: any) => data.genres)) }
缓存服务的数据结构
private cache: Map<string, [Date | null, HttpResponse<unknown>]> = new Map()
该服务配有对应的getter和setter方法。
HTTP拦截器实现代码
const HALF_HOUR = 1800 const TIME_TO_LIVE = HALF_HOUR @Injectable() export class CachingInterceptor implements HttpInterceptor { constructor(private cacheResolver: CacheResolverService) {} intercept( request: HttpRequest<unknown>, next: HttpHandler ): Observable<HttpEvent<unknown>> { if (request.method !== 'GET') { return next.handle(request) } console.log('CachingInterceptor: ' + request.url) const cachedResponse = this.cacheResolver.get(request.url) console.log('CachedResponse:' + JSON.stringify(cachedResponse?.body)) return cachedResponse ? of(cachedResponse) : this.sendRequest(request, next) } sendRequest( request: HttpRequest<unknown>, next: HttpHandler ): Observable<HttpEvent<unknown>> { return next.handle(request).pipe( map((event: HttpEvent<unknown>) => { if (event instanceof HttpResponse) { this.cacheResolver.set(request.url, event, TIME_TO_LIVE) } return event }) ) } }
触发的错误
当getSeriesWithGenres()方法被调用两次时,抛出如下错误:
TypeError: "genre_ids" is read-only
问题根源
Angular的HttpResponse对象默认是**不可变(immutable)**的,缓存的响应体同样处于只读状态。第一次调用方法时,直接修改了响应体中的serie.genre_ids属性;第二次调用时,从缓存取出的是同一个只读对象,再次尝试修改该属性就会触发只读限制错误。
解决方案
方案1:修改数据时创建对象副本
在getSeriesWithGenres()的map操作中,不直接修改原响应对象,而是创建新的对象副本:
getSeriesWithGenres() { const series$ = this.getSeries() const genres$ = this.getGenres() return forkJoin([series$, genres$]).pipe( map(([series, genres]) => { return series.map((serie: Serie) => { // 展开原对象创建副本,避免修改只读的缓存对象 return { ...serie, genre_ids: serie.genre_ids.map(genreId => { return genres.find((genre: Genre) => genre.id === genreId)?.name }) } }) }) ) }
方案2:缓存时创建响应的可变副本
在拦截器缓存响应前,克隆HttpResponse并将响应体转为可变对象:
sendRequest( request: HttpRequest<unknown>, next: HttpHandler ): Observable<HttpEvent<unknown>> { return next.handle(request).pipe( map((event: HttpEvent<unknown>) => { if (event instanceof HttpResponse) { // 克隆响应并深拷贝响应体,创建可变副本 const mutableResponse = event.clone({ body: JSON.parse(JSON.stringify(event.body)) }) this.cacheResolver.set(request.url, mutableResponse, TIME_TO_LIVE) return mutableResponse } return event }) ) }
注意:JSON.parse(JSON.stringify())是简易深拷贝方式,若响应体包含函数、循环引用等复杂结构,需替换为更可靠的深拷贝方法(如Lodash的cloneDeep)。
方案3:调整缓存层级(推荐)
不在拦截器层面缓存原始HTTP响应,而是在业务服务中缓存处理后的数据。既避免修改只读对象的问题,也让缓存逻辑更贴合业务需求:
// 在业务服务中添加缓存 private seriesWithGenresCache: Serie[] | null = null; getSeriesWithGenres() { if (this.seriesWithGenresCache) { return of(this.seriesWithGenresCache); } const series$ = this.getSeries() const genres$ = this.getGenres() return forkJoin([series$, genres$]).pipe( map(([series, genres]) => { const processedSeries = series.map((serie: Serie) => { return { ...serie, genre_ids: serie.genre_ids.map(genreId => { return genres.find((genre: Genre) => genre.id === genreId)?.name }) } }); // 缓存处理后的数据 this.seriesWithGenresCache = processedSeries; return processedSeries; }) ) }
内容的提问来源于stack exchange,提问作者nhussein11
相关产品推荐
相关产品推荐

