diff --git a/src/HttpClient/HttpClient.ts b/src/HttpClient/HttpClient.ts index 64bdb8e6d..264a512a0 100644 --- a/src/HttpClient/HttpClient.ts +++ b/src/HttpClient/HttpClient.ts @@ -10,6 +10,7 @@ import { FORWARDED_HOST_HEADER, LOCALE_HEADER, PRODUCT_HEADER, + SEARCH_SEGMENT_HEADER, SEGMENT_HEADER, SESSION_HEADER, TENANT_HEADER, @@ -56,6 +57,7 @@ export class HttpClient { userAgent, timeout = DEFAULT_TIMEOUT_MS, segmentToken, + searchSegmentToken, sessionToken, retries, concurrency, @@ -88,6 +90,7 @@ export class HttpClient { ...operationId ? { 'x-vtex-operation-id': operationId } : null, ...product ? { [PRODUCT_HEADER]: product } : null, ...segmentToken ? { [SEGMENT_HEADER]: segmentToken } : null, + ...searchSegmentToken ? { [SEARCH_SEGMENT_HEADER]: searchSegmentToken } : null, ...sessionToken ? { [SESSION_HEADER]: sessionToken } : null, } @@ -131,16 +134,16 @@ export class HttpClient { return typeof v !== 'object' || v === null || Array.isArray(v) ? v : Object.fromEntries(Object.entries(v).sort(([ka], [kb]) => ka < kb ? -1 : ka > kb ? 1 : 0)) - } - catch(error) { + } + catch(error) { // I don't believe this will ever happen, but just in case // Also, I didn't include error as I am unsure if it would have sensitive information this.logger.warn({message: 'Error while sorting object for cache key'}) return v } } - - + + const bodyHash = createHash('md5').update(JSON.stringify(data, deterministicReplacer)).digest('hex') const cacheableConfig = this.getConfig(url, { ...config, diff --git a/src/HttpClient/middlewares/cache.ts b/src/HttpClient/middlewares/cache.ts index 6af403290..2081650a6 100644 --- a/src/HttpClient/middlewares/cache.ts +++ b/src/HttpClient/middlewares/cache.ts @@ -1,7 +1,7 @@ import { AxiosRequestConfig, AxiosResponse } from 'axios' import { CacheLayer } from '../../caches/CacheLayer' -import { LOCALE_HEADER, SEGMENT_HEADER, SESSION_HEADER } from '../../constants' +import { LOCALE_HEADER, SEARCH_SEGMENT_HEADER, SEGMENT_HEADER, SESSION_HEADER } from '../../constants' import { HttpLogEvents } from '../../tracing/LogEvents' import { HttpCacheLogFields } from '../../tracing/LogFields' import { CustomHttpTags } from '../../tracing/Tags' @@ -11,7 +11,7 @@ const RANGE_HEADER_QS_KEY = '__range_header' const cacheableStatusCodes = [200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501] // https://tools.ietf.org/html/rfc7231#section-6.1 export const cacheKey = (config: AxiosRequestConfig) => { - const {baseURL = '', url = '', params, headers} = config + const { baseURL = '', url = '', params, headers } = config const locale = headers[LOCALE_HEADER] const encodedBaseURL = baseURL.replace(/\//g, '\\') @@ -20,9 +20,9 @@ export const cacheKey = (config: AxiosRequestConfig) => { let key = `${locale}--${encodedBaseURL}--${encodedURL}?` if (params) { - Object.keys(params).sort().forEach(p => - key = key.concat(`--${p}=${params[p]}`) - ) + Object.keys(params) + .sort() + .forEach((p) => (key = key.concat(`--${p}=${params[p]}`))) } if (headers?.range) { key = key.concat(`--${RANGE_HEADER_QS_KEY}=${headers.range}`) @@ -32,9 +32,9 @@ export const cacheKey = (config: AxiosRequestConfig) => { } const parseCacheHeaders = (headers: Record) => { - const {'cache-control': cacheControl = '', etag, age: ageStr} = headers - const cacheDirectives = cacheControl.split(',').map(d => d.trim()) - const maxAgeDirective = cacheDirectives.find(d => d.startsWith('max-age')) + const { 'cache-control': cacheControl = '', etag, age: ageStr } = headers + const cacheDirectives = cacheControl.split(',').map((d) => d.trim()) + const maxAgeDirective = cacheDirectives.find((d) => d.startsWith('max-age')) const [, maxAgeStr] = maxAgeDirective ? maxAgeDirective.split('=') : [null, null] const maxAge = maxAgeStr ? parseInt(maxAgeStr, 10) : 0 const age = ageStr ? parseInt(ageStr, 10) : 0 @@ -48,13 +48,12 @@ const parseCacheHeaders = (headers: Record) => { } } -export function isLocallyCacheable (arg: RequestConfig, type: CacheType): arg is CacheableRequestConfig { - return arg && !!arg.cacheable - && (arg.cacheable === type || arg.cacheable === CacheType.Any || type === CacheType.Any) +export function isLocallyCacheable(arg: RequestConfig, type: CacheType): arg is CacheableRequestConfig { + return arg && !!arg.cacheable && (arg.cacheable === type || arg.cacheable === CacheType.Any || type === CacheType.Any) } -const addNotModified = (validateStatus: (status: number) => boolean) => - (status: number) => validateStatus(status) || status === 304 +const addNotModified = (validateStatus: (status: number) => boolean) => (status: number) => + validateStatus(status) || status === 304 export enum CacheType { None, @@ -82,7 +81,8 @@ interface CacheOptions { } export const cacheMiddleware = ({ type, storage }: CacheOptions) => { - const CACHE_RESULT_TAG = type === CacheType.Disk ? CustomHttpTags.HTTP_DISK_CACHE_RESULT : CustomHttpTags.HTTP_MEMORY_CACHE_RESULT + const CACHE_RESULT_TAG = + type === CacheType.Disk ? CustomHttpTags.HTTP_DISK_CACHE_RESULT : CustomHttpTags.HTTP_MEMORY_CACHE_RESULT const cacheType = CacheTypeNames[type] return async (ctx: MiddlewareContext, next: () => Promise) => { @@ -93,21 +93,24 @@ export const cacheMiddleware = ({ type, storage }: CacheOptions) => { const span = ctx.tracing!.rootSpan const key = cacheKey(ctx.config) - const segmentToken = ctx.config.headers[SEGMENT_HEADER] - const keyWithSegment = key + segmentToken + const segmentToken = ctx.config.headers[SEGMENT_HEADER] ?? '' + const searchSegmentToken = ctx.config.headers[SEARCH_SEGMENT_HEADER] ?? '' + const keyWithSegment = `${key}${segmentToken}` + const keyWithSegmentAndSearchSegment = `${keyWithSegment}${searchSegmentToken}` span.log({ event: HttpLogEvents.CACHE_KEY_CREATE, [HttpCacheLogFields.CACHE_TYPE]: cacheType, [HttpCacheLogFields.KEY]: key, - [HttpCacheLogFields.KEY_WITH_SEGMENT]: keyWithSegment, + [HttpCacheLogFields.KEY_WITH_SEGMENT]: `${key}${segmentToken}`, + [HttpCacheLogFields.KEY_WITH_SEGMENT_AND_SEARCH_SEGMENT]: keyWithSegmentAndSearchSegment, }) - const cacheHasWithSegment = await storage.has(keyWithSegment) - const cached = cacheHasWithSegment ? await storage.get(keyWithSegment) : await storage.get(key) + const hasCache = await storage.has(keyWithSegmentAndSearchSegment) + const cached = hasCache ? await storage.get(keyWithSegmentAndSearchSegment) : null if (cached && cached.response) { - const {etag: cachedEtag, response, expiration, responseType, responseEncoding} = cached as Cached + const { etag: cachedEtag, response, expiration, responseType, responseEncoding } = cached as Cached if (type === CacheType.Disk && responseType === 'arraybuffer') { response.data = Buffer.from(response.data, responseEncoding) @@ -119,7 +122,7 @@ export const cacheMiddleware = ({ type, storage }: CacheOptions) => { event: HttpLogEvents.LOCAL_CACHE_HIT_INFO, [HttpCacheLogFields.CACHE_TYPE]: cacheType, [HttpCacheLogFields.ETAG]: cachedEtag, - [HttpCacheLogFields.EXPIRATION_TIME]: (expiration-now)/1000, + [HttpCacheLogFields.EXPIRATION_TIME]: (expiration - now) / 1000, [HttpCacheLogFields.RESPONSE_TYPE]: responseType, [HttpCacheLogFields.RESPONSE_ENCONDING]: responseEncoding, }) @@ -162,11 +165,12 @@ export const cacheMiddleware = ({ type, storage }: CacheOptions) => { } } - const {data, headers, status} = ctx.response as AxiosResponse - const {age, etag, maxAge: headerMaxAge, noStore, noCache} = parseCacheHeaders(headers) + const { data, headers, status } = ctx.response as AxiosResponse + const { age, etag, maxAge: headerMaxAge, noStore, noCache } = parseCacheHeaders(headers) - const {forceMaxAge} = ctx.config - const maxAge = forceMaxAge && cacheableStatusCodes.includes(status) ? Math.max(forceMaxAge, headerMaxAge) : headerMaxAge + const { forceMaxAge } = ctx.config + const maxAge = + forceMaxAge && cacheableStatusCodes.includes(status) ? Math.max(forceMaxAge, headerMaxAge) : headerMaxAge span.log({ event: HttpLogEvents.CACHE_CONFIG, @@ -189,20 +193,22 @@ export const cacheMiddleware = ({ type, storage }: CacheOptions) => { const shouldCache = maxAge || etag const varySession = ctx.response.headers.vary && ctx.response.headers.vary.includes(SESSION_HEADER) if (shouldCache && !varySession) { - const {responseType, responseEncoding: configResponseEncoding} = ctx.config + const { responseType, responseEncoding: configResponseEncoding } = ctx.config const currentAge = revalidated ? 0 : age const varySegment = ctx.response.headers.vary && ctx.response.headers.vary.includes(SEGMENT_HEADER) - const setKey = varySegment ? keyWithSegment : key + const varySearchSegment = ctx.response.headers.vary && ctx.response.headers.vary.includes(SEARCH_SEGMENT_HEADER) + + const setKey = `${key}${varySegment ? segmentToken : ''}${varySearchSegment ? searchSegmentToken : ''}` + const responseEncoding = configResponseEncoding || (responseType === 'arraybuffer' ? 'base64' : undefined) - const cacheableData = type === CacheType.Disk && responseType === 'arraybuffer' - ? (data as Buffer).toString(responseEncoding) - : data + const cacheableData = + type === CacheType.Disk && responseType === 'arraybuffer' ? (data as Buffer).toString(responseEncoding) : data const expiration = Date.now() + (maxAge - currentAge) * 1000 await storage.set(setKey, { etag, expiration, - response: {data: cacheableData, headers, status}, + response: { data: cacheableData, headers, status }, responseEncoding, responseType, }) @@ -213,7 +219,7 @@ export const cacheMiddleware = ({ type, storage }: CacheOptions) => { [HttpCacheLogFields.KEY_SET]: setKey, [HttpCacheLogFields.AGE]: currentAge, [HttpCacheLogFields.ETAG]: etag, - [HttpCacheLogFields.EXPIRATION_TIME]: (expiration - Date.now())/1000, + [HttpCacheLogFields.EXPIRATION_TIME]: (expiration - Date.now()) / 1000, [HttpCacheLogFields.RESPONSE_ENCONDING]: responseEncoding, [HttpCacheLogFields.RESPONSE_TYPE]: responseType, }) @@ -234,7 +240,7 @@ export interface Cached { } export type CacheableRequestConfig = RequestConfig & { - url: string, - cacheable: CacheType, + url: string + cacheable: CacheType memoizable: boolean } diff --git a/src/constants.ts b/src/constants.ts index 003156604..b7512a3af 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,6 +9,7 @@ export const PID = process.pid export const CACHE_CONTROL_HEADER = 'cache-control' export const SEGMENT_HEADER = 'x-vtex-segment' +export const SEARCH_SEGMENT_HEADER = 'x-vtex-search-segment' export const SESSION_HEADER = 'x-vtex-session' export const PRODUCT_HEADER = 'x-vtex-product' export const LOCALE_HEADER = 'x-vtex-locale' @@ -36,7 +37,7 @@ export const COLOSSUS_PARAMS_HEADER = 'x-colossus-params' export const TRACE_ID_HEADER = 'x-trace-id' export const PROVIDER_HEADER = 'x-vtex-provider' -export type VaryHeaders = typeof SEGMENT_HEADER | typeof SESSION_HEADER | typeof PRODUCT_HEADER | typeof LOCALE_HEADER +export type VaryHeaders = typeof SEGMENT_HEADER | typeof SESSION_HEADER | typeof PRODUCT_HEADER | typeof LOCALE_HEADER | typeof SEARCH_SEGMENT_HEADER export const BODY_HASH = '__graphqlBodyHash' diff --git a/src/service/worker/runtime/graphql/middlewares/response.ts b/src/service/worker/runtime/graphql/middlewares/response.ts index cdfbad6b8..26bf49652 100644 --- a/src/service/worker/runtime/graphql/middlewares/response.ts +++ b/src/service/worker/runtime/graphql/middlewares/response.ts @@ -3,11 +3,10 @@ import { ETAG_HEADER, FORWARDED_HOST_HEADER, META_HEADER, + SEARCH_SEGMENT_HEADER, SEGMENT_HEADER, SESSION_HEADER, } from '../../../../../constants' -import { Maybe } from '../../typings' -import { Recorder } from '../../utils/recorder' import { GraphQLCacheControl, GraphQLServiceContext } from '../typings' import { cacheControlHTTP } from '../utils/cacheControl' @@ -17,8 +16,14 @@ function setVaryHeaders (ctx: GraphQLServiceContext, cacheControl: GraphQLCacheC ctx.vary(SEGMENT_HEADER) } + if (cacheControl.scope === 'search_segment') { + ctx.vary(SEGMENT_HEADER) + ctx.vary(SEARCH_SEGMENT_HEADER) + } + if (cacheControl.scope === 'private' || ctx.query.scope === 'private') { ctx.vary(SEGMENT_HEADER) + ctx.vary(SEARCH_SEGMENT_HEADER) ctx.vary(SESSION_HEADER) } else if (ctx.vtex.sessionToken) { ctx.vtex.logger.warn({ diff --git a/src/service/worker/runtime/graphql/typings.ts b/src/service/worker/runtime/graphql/typings.ts index 0ff06b899..d04aa05b3 100644 --- a/src/service/worker/runtime/graphql/typings.ts +++ b/src/service/worker/runtime/graphql/typings.ts @@ -15,7 +15,7 @@ export type GraphQLResponse = TypeFromPromise> export interface GraphQLCacheControl { maxAge: number - scope: 'private' | 'public' | 'segment' + scope: 'private' | 'public' | 'segment' | 'search_segment' noCache: boolean noStore: boolean } diff --git a/src/service/worker/runtime/http/middlewares/error.ts b/src/service/worker/runtime/http/middlewares/error.ts index 9b68ed90a..addd2bc1b 100644 --- a/src/service/worker/runtime/http/middlewares/error.ts +++ b/src/service/worker/runtime/http/middlewares/error.ts @@ -1,5 +1,5 @@ import { IOClients } from '../../../../../clients/IOClients' -import { LINKED } from '../../../../../constants' +import { LINKED, SEARCH_SEGMENT_HEADER, SEGMENT_HEADER } from '../../../../../constants' import { cancelledRequestStatus, RequestCancelledError, diff --git a/src/service/worker/runtime/http/middlewares/vary.ts b/src/service/worker/runtime/http/middlewares/vary.ts index e21b76963..67d1fa0a3 100644 --- a/src/service/worker/runtime/http/middlewares/vary.ts +++ b/src/service/worker/runtime/http/middlewares/vary.ts @@ -1,6 +1,7 @@ import { IOClients } from '../../../../../clients/IOClients' import { LOCALE_HEADER, + SEARCH_SEGMENT_HEADER, SEGMENT_HEADER, SESSION_HEADER, VaryHeaders, @@ -17,18 +18,23 @@ const cachingStrategies: CachingStrategy[] = [ { forbidden: [], path: '/_v/private/', - vary: [SEGMENT_HEADER, SESSION_HEADER], + vary: [SEGMENT_HEADER, SEARCH_SEGMENT_HEADER, SESSION_HEADER], }, { - forbidden: [SEGMENT_HEADER, SESSION_HEADER], + forbidden: [SEGMENT_HEADER, SEARCH_SEGMENT_HEADER, SESSION_HEADER], path: '/_v/public/', vary: [], }, { - forbidden: [SESSION_HEADER], + forbidden: [SESSION_HEADER, SEARCH_SEGMENT_HEADER], path: '/_v/segment/', vary: [SEGMENT_HEADER], }, + { + forbidden: [SESSION_HEADER], + path: '/_v/search-segment/', + vary: [SEGMENT_HEADER, SEARCH_SEGMENT_HEADER], + }, ] const shouldVaryByHeader = ( @@ -47,12 +53,10 @@ const shouldVaryByHeader = (ctx: ServiceContext, next: () => Promise) { +export async function vary( + ctx: ServiceContext, + next: () => Promise +) { const { method, path } = ctx const strategy = cachingStrategies.find((cachingStrategy) => path.indexOf(cachingStrategy.path) === 0) @@ -62,7 +66,6 @@ export async function vary < }) } - // We don't need to vary non GET requests, since they are never cached if (method.toUpperCase() !== 'GET') { await next() @@ -76,6 +79,9 @@ export async function vary < if (shouldVaryByHeader(ctx, SESSION_HEADER, strategy)) { ctx.vary(SESSION_HEADER) } + if (shouldVaryByHeader(ctx, SEARCH_SEGMENT_HEADER, strategy)) { + ctx.vary(SEARCH_SEGMENT_HEADER) + } await next() } diff --git a/src/service/worker/runtime/typings.ts b/src/service/worker/runtime/typings.ts index 8392cbe64..c36273d71 100644 --- a/src/service/worker/runtime/typings.ts +++ b/src/service/worker/runtime/typings.ts @@ -137,6 +137,7 @@ export interface IOContext { userAgent: string workspace: string segmentToken?: string + searchSegmentToken?: string sessionToken?: string requestId: string operationId: string diff --git a/src/service/worker/runtime/utils/context.ts b/src/service/worker/runtime/utils/context.ts index 6fe1f1408..05a19a5ef 100644 --- a/src/service/worker/runtime/utils/context.ts +++ b/src/service/worker/runtime/utils/context.ts @@ -13,6 +13,7 @@ import { REGION, REQUEST_ID_HEADER, SEGMENT_HEADER, + SEARCH_SEGMENT_HEADER, SESSION_HEADER, TENANT_HEADER, WORKSPACE_HEADER, @@ -44,6 +45,7 @@ export const prepareHandlerCtx = (header: Context['request']['header'], tracingC region: REGION, requestId: header[REQUEST_ID_HEADER], segmentToken: header[SEGMENT_HEADER], + searchSegmentToken: header[SEARCH_SEGMENT_HEADER], sessionToken: header[SESSION_HEADER], tenant: header[TENANT_HEADER] ? parseTenantHeaderValue(header[TENANT_HEADER]) : undefined, tracer: new UserLandTracer(tracingContext.tracer, tracingContext.currentSpan), diff --git a/src/tracing/LogFields.ts b/src/tracing/LogFields.ts index c65b3c0ec..a7ef296f4 100644 --- a/src/tracing/LogFields.ts +++ b/src/tracing/LogFields.ts @@ -36,6 +36,9 @@ export const enum HttpCacheLogFields { /** The generated cache key for local cache with the segment added to it */ KEY_WITH_SEGMENT = 'key-with-segment', + /** The generated cache key for local cache with the segment and the searchSegment added to it */ + KEY_WITH_SEGMENT_AND_SEARCH_SEGMENT = 'key-with-segment-and-search-segment', + /** The key that was just set on the cache */ KEY_SET = 'key-set',