mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-12 23:16:26 +00:00
feat(http): Add pluggable HTTP cache implementation (#27998)
This commit is contained in:
parent
b900884825
commit
4f70ff15cd
9 changed files with 391 additions and 41 deletions
10
lib/util/cache/repository/types.ts
vendored
10
lib/util/cache/repository/types.ts
vendored
|
@ -6,7 +6,6 @@ import type {
|
||||||
import type { PackageFile } from '../../../modules/manager/types';
|
import type { PackageFile } from '../../../modules/manager/types';
|
||||||
import type { RepoInitConfig } from '../../../workers/repository/init/types';
|
import type { RepoInitConfig } from '../../../workers/repository/init/types';
|
||||||
import type { PrBlockedBy } from '../../../workers/types';
|
import type { PrBlockedBy } from '../../../workers/types';
|
||||||
import type { HttpResponse } from '../../http/types';
|
|
||||||
|
|
||||||
export interface BaseBranchCache {
|
export interface BaseBranchCache {
|
||||||
sha: string; // branch commit sha
|
sha: string; // branch commit sha
|
||||||
|
@ -123,16 +122,9 @@ export interface BranchCache {
|
||||||
result?: string;
|
result?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HttpCache {
|
|
||||||
etag?: string;
|
|
||||||
httpResponse: HttpResponse<unknown>;
|
|
||||||
lastModified?: string;
|
|
||||||
timeStamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RepoCacheData {
|
export interface RepoCacheData {
|
||||||
configFileName?: string;
|
configFileName?: string;
|
||||||
httpCache?: Record<string, HttpCache>;
|
httpCache?: Record<string, unknown>;
|
||||||
semanticCommits?: 'enabled' | 'disabled';
|
semanticCommits?: 'enabled' | 'disabled';
|
||||||
branches?: BranchCache[];
|
branches?: BranchCache[];
|
||||||
init?: RepoInitConfig;
|
init?: RepoInitConfig;
|
||||||
|
|
94
lib/util/http/cache/abstract-http-cache-provider.ts
vendored
Normal file
94
lib/util/http/cache/abstract-http-cache-provider.ts
vendored
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import { logger } from '../../../logger';
|
||||||
|
import { HttpCacheStats } from '../../stats';
|
||||||
|
import type { GotOptions, HttpResponse } from '../types';
|
||||||
|
import { copyResponse } from '../util';
|
||||||
|
import { HttpCacheSchema } from './schema';
|
||||||
|
import type { HttpCache, HttpCacheProvider } from './types';
|
||||||
|
|
||||||
|
export abstract class AbstractHttpCacheProvider implements HttpCacheProvider {
|
||||||
|
protected abstract load(url: string): Promise<unknown>;
|
||||||
|
protected abstract persist(url: string, data: HttpCache): Promise<void>;
|
||||||
|
|
||||||
|
async get(url: string): Promise<HttpCache | null> {
|
||||||
|
const cache = await this.load(url);
|
||||||
|
const httpCache = HttpCacheSchema.parse(cache);
|
||||||
|
if (!httpCache) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpCache as HttpCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCacheHeaders<T extends Pick<GotOptions, 'headers'>>(
|
||||||
|
url: string,
|
||||||
|
opts: T,
|
||||||
|
): Promise<void> {
|
||||||
|
const httpCache = await this.get(url);
|
||||||
|
if (!httpCache) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.headers ??= {};
|
||||||
|
|
||||||
|
if (httpCache.etag) {
|
||||||
|
opts.headers['If-None-Match'] = httpCache.etag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (httpCache.lastModified) {
|
||||||
|
opts.headers['If-Modified-Since'] = httpCache.lastModified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async wrapResponse<T>(
|
||||||
|
url: string,
|
||||||
|
resp: HttpResponse<T>,
|
||||||
|
): Promise<HttpResponse<T>> {
|
||||||
|
if (resp.statusCode === 200) {
|
||||||
|
const etag = resp.headers?.['etag'];
|
||||||
|
const lastModified = resp.headers?.['last-modified'];
|
||||||
|
|
||||||
|
HttpCacheStats.incRemoteMisses(url);
|
||||||
|
|
||||||
|
const httpResponse = copyResponse(resp, true);
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
const newHttpCache = HttpCacheSchema.parse({
|
||||||
|
etag,
|
||||||
|
lastModified,
|
||||||
|
httpResponse,
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
if (newHttpCache) {
|
||||||
|
logger.debug(
|
||||||
|
`http cache: saving ${url} (etag=${etag}, lastModified=${lastModified})`,
|
||||||
|
);
|
||||||
|
await this.persist(url, newHttpCache as HttpCache);
|
||||||
|
} else {
|
||||||
|
logger.debug(`http cache: failed to persist cache for ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.statusCode === 304) {
|
||||||
|
const httpCache = await this.get(url);
|
||||||
|
if (!httpCache) {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = httpCache.timestamp;
|
||||||
|
logger.debug(
|
||||||
|
`http cache: Using cached response: ${url} from ${timestamp}`,
|
||||||
|
);
|
||||||
|
HttpCacheStats.incRemoteHits(url);
|
||||||
|
const cachedResp = copyResponse(
|
||||||
|
httpCache.httpResponse as HttpResponse<T>,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
cachedResp.authorization = resp.authorization;
|
||||||
|
return cachedResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
152
lib/util/http/cache/repository-http-cache-provider.spec.ts
vendored
Normal file
152
lib/util/http/cache/repository-http-cache-provider.spec.ts
vendored
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
import { Http } from '..';
|
||||||
|
import * as httpMock from '../../../../test/http-mock';
|
||||||
|
import { logger } from '../../../../test/util';
|
||||||
|
import { getCache, resetCache } from '../../cache/repository';
|
||||||
|
import { repoCacheProvider } from './repository-http-cache-provider';
|
||||||
|
|
||||||
|
describe('util/http/cache/repository-http-cache-provider', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
const http = new Http('test', {
|
||||||
|
cacheProvider: repoCacheProvider,
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reuses data with etag', async () => {
|
||||||
|
const scope = httpMock.scope('https://example.com');
|
||||||
|
|
||||||
|
scope.get('/foo/bar').reply(200, { msg: 'Hello, world!' }, { etag: '123' });
|
||||||
|
const res1 = await http.getJson('https://example.com/foo/bar');
|
||||||
|
expect(res1).toMatchObject({
|
||||||
|
statusCode: 200,
|
||||||
|
body: { msg: 'Hello, world!' },
|
||||||
|
authorization: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.get('/foo/bar').reply(304);
|
||||||
|
const res2 = await http.getJson('https://example.com/foo/bar');
|
||||||
|
expect(res2).toMatchObject({
|
||||||
|
statusCode: 200,
|
||||||
|
body: { msg: 'Hello, world!' },
|
||||||
|
authorization: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reuses data with last-modified', async () => {
|
||||||
|
const scope = httpMock.scope('https://example.com');
|
||||||
|
|
||||||
|
scope
|
||||||
|
.get('/foo/bar')
|
||||||
|
.reply(
|
||||||
|
200,
|
||||||
|
{ msg: 'Hello, world!' },
|
||||||
|
{ 'last-modified': 'Mon, 01 Jan 2000 00:00:00 GMT' },
|
||||||
|
);
|
||||||
|
const res1 = await http.getJson('https://example.com/foo/bar');
|
||||||
|
expect(res1).toMatchObject({
|
||||||
|
statusCode: 200,
|
||||||
|
body: { msg: 'Hello, world!' },
|
||||||
|
authorization: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.get('/foo/bar').reply(304);
|
||||||
|
const res2 = await http.getJson('https://example.com/foo/bar');
|
||||||
|
expect(res2).toMatchObject({
|
||||||
|
statusCode: 200,
|
||||||
|
body: { msg: 'Hello, world!' },
|
||||||
|
authorization: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses older cache format', async () => {
|
||||||
|
const repoCache = getCache();
|
||||||
|
repoCache.httpCache = {
|
||||||
|
'https://example.com/foo/bar': {
|
||||||
|
etag: '123',
|
||||||
|
lastModified: 'Mon, 01 Jan 2000 00:00:00 GMT',
|
||||||
|
httpResponse: { statusCode: 200, body: { msg: 'Hello, world!' } },
|
||||||
|
timeStamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
httpMock.scope('https://example.com').get('/foo/bar').reply(304);
|
||||||
|
|
||||||
|
const res = await http.getJson('https://example.com/foo/bar');
|
||||||
|
|
||||||
|
expect(res).toMatchObject({
|
||||||
|
statusCode: 200,
|
||||||
|
body: { msg: 'Hello, world!' },
|
||||||
|
authorization: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports if cache could not be persisted', async () => {
|
||||||
|
httpMock
|
||||||
|
.scope('https://example.com')
|
||||||
|
.get('/foo/bar')
|
||||||
|
.reply(200, { msg: 'Hello, world!' });
|
||||||
|
|
||||||
|
await http.getJson('https://example.com/foo/bar');
|
||||||
|
|
||||||
|
expect(logger.logger.debug).toHaveBeenCalledWith(
|
||||||
|
'http cache: failed to persist cache for https://example.com/foo/bar',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles abrupt cache reset', async () => {
|
||||||
|
const scope = httpMock.scope('https://example.com');
|
||||||
|
|
||||||
|
scope.get('/foo/bar').reply(200, { msg: 'Hello, world!' }, { etag: '123' });
|
||||||
|
const res1 = await http.getJson('https://example.com/foo/bar');
|
||||||
|
expect(res1).toMatchObject({
|
||||||
|
statusCode: 200,
|
||||||
|
body: { msg: 'Hello, world!' },
|
||||||
|
authorization: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
resetCache();
|
||||||
|
|
||||||
|
scope.get('/foo/bar').reply(304);
|
||||||
|
const res2 = await http.getJson('https://example.com/foo/bar');
|
||||||
|
expect(res2).toMatchObject({
|
||||||
|
statusCode: 304,
|
||||||
|
authorization: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bypasses for statuses other than 200 and 304', async () => {
|
||||||
|
const scope = httpMock.scope('https://example.com');
|
||||||
|
scope.get('/foo/bar').reply(203);
|
||||||
|
|
||||||
|
const res = await http.getJson('https://example.com/foo/bar');
|
||||||
|
|
||||||
|
expect(res).toMatchObject({
|
||||||
|
statusCode: 203,
|
||||||
|
authorization: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports authorization', async () => {
|
||||||
|
const scope = httpMock.scope('https://example.com');
|
||||||
|
|
||||||
|
scope.get('/foo/bar').reply(200, { msg: 'Hello, world!' }, { etag: '123' });
|
||||||
|
const res1 = await http.getJson('https://example.com/foo/bar', {
|
||||||
|
headers: { authorization: 'Bearer 123' },
|
||||||
|
});
|
||||||
|
expect(res1).toMatchObject({
|
||||||
|
statusCode: 200,
|
||||||
|
body: { msg: 'Hello, world!' },
|
||||||
|
authorization: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.get('/foo/bar').reply(304);
|
||||||
|
const res2 = await http.getJson('https://example.com/foo/bar', {
|
||||||
|
headers: { authorization: 'Bearer 123' },
|
||||||
|
});
|
||||||
|
expect(res2).toMatchObject({
|
||||||
|
statusCode: 200,
|
||||||
|
body: { msg: 'Hello, world!' },
|
||||||
|
authorization: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
20
lib/util/http/cache/repository-http-cache-provider.ts
vendored
Normal file
20
lib/util/http/cache/repository-http-cache-provider.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { getCache } from '../../cache/repository';
|
||||||
|
import { AbstractHttpCacheProvider } from './abstract-http-cache-provider';
|
||||||
|
import type { HttpCache } from './types';
|
||||||
|
|
||||||
|
export class RepositoryHttpCacheProvider extends AbstractHttpCacheProvider {
|
||||||
|
override load(url: string): Promise<unknown> {
|
||||||
|
const cache = getCache();
|
||||||
|
cache.httpCache ??= {};
|
||||||
|
return Promise.resolve(cache.httpCache[url]);
|
||||||
|
}
|
||||||
|
|
||||||
|
override persist(url: string, data: HttpCache): Promise<void> {
|
||||||
|
const cache = getCache();
|
||||||
|
cache.httpCache ??= {};
|
||||||
|
cache.httpCache[url] = data;
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const repoCacheProvider = new RepositoryHttpCacheProvider();
|
34
lib/util/http/cache/schema.ts
vendored
Normal file
34
lib/util/http/cache/schema.ts
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const invalidFieldsMsg =
|
||||||
|
'Cache object should have `etag` or `lastModified` fields';
|
||||||
|
|
||||||
|
export const HttpCacheSchema = z
|
||||||
|
.object({
|
||||||
|
// TODO: remove this migration part during the Christmas eve 2024
|
||||||
|
timeStamp: z.string().optional(),
|
||||||
|
timestamp: z.string().optional(),
|
||||||
|
})
|
||||||
|
.passthrough()
|
||||||
|
.transform((data) => {
|
||||||
|
if (data.timeStamp) {
|
||||||
|
data.timestamp = data.timeStamp;
|
||||||
|
delete data.timeStamp;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
etag: z.string().optional(),
|
||||||
|
lastModified: z.string().optional(),
|
||||||
|
httpResponse: z.unknown(),
|
||||||
|
timestamp: z.string(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
({ etag, lastModified }) => etag ?? lastModified,
|
||||||
|
invalidFieldsMsg,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.nullable()
|
||||||
|
.catch(null);
|
17
lib/util/http/cache/types.ts
vendored
Normal file
17
lib/util/http/cache/types.ts
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import type { GotOptions, HttpResponse } from '../types';
|
||||||
|
|
||||||
|
export interface HttpCache {
|
||||||
|
etag?: string;
|
||||||
|
lastModified?: string;
|
||||||
|
httpResponse: unknown;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HttpCacheProvider {
|
||||||
|
setCacheHeaders<T extends Pick<GotOptions, 'headers'>>(
|
||||||
|
url: string,
|
||||||
|
opts: T,
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
wrapResponse<T>(url: string, resp: HttpResponse<T>): Promise<HttpResponse<T>>;
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import is from '@sindresorhus/is';
|
||||||
import merge from 'deepmerge';
|
import merge from 'deepmerge';
|
||||||
import got, { Options, RequestError } from 'got';
|
import got, { Options, RequestError } from 'got';
|
||||||
import type { SetRequired } from 'type-fest';
|
import type { SetRequired } from 'type-fest';
|
||||||
|
@ -8,7 +9,6 @@ import { logger } from '../../logger';
|
||||||
import { ExternalHostError } from '../../types/errors/external-host-error';
|
import { ExternalHostError } from '../../types/errors/external-host-error';
|
||||||
import * as memCache from '../cache/memory';
|
import * as memCache from '../cache/memory';
|
||||||
import { getCache } from '../cache/repository';
|
import { getCache } from '../cache/repository';
|
||||||
import { clone } from '../clone';
|
|
||||||
import { hash } from '../hash';
|
import { hash } from '../hash';
|
||||||
import { type AsyncResult, Result } from '../result';
|
import { type AsyncResult, Result } from '../result';
|
||||||
import {
|
import {
|
||||||
|
@ -27,12 +27,14 @@ import type {
|
||||||
GotJSONOptions,
|
GotJSONOptions,
|
||||||
GotOptions,
|
GotOptions,
|
||||||
GotTask,
|
GotTask,
|
||||||
|
HttpCache,
|
||||||
HttpOptions,
|
HttpOptions,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
InternalHttpOptions,
|
InternalHttpOptions,
|
||||||
} from './types';
|
} from './types';
|
||||||
// TODO: refactor code to remove this (#9651)
|
// TODO: refactor code to remove this (#9651)
|
||||||
import './legacy';
|
import './legacy';
|
||||||
|
import { copyResponse } from './util';
|
||||||
|
|
||||||
export { RequestError as HttpError };
|
export { RequestError as HttpError };
|
||||||
|
|
||||||
|
@ -49,26 +51,6 @@ type JsonArgs<
|
||||||
schema?: Schema;
|
schema?: Schema;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Copying will help to avoid circular structure
|
|
||||||
// and mutation of the cached response.
|
|
||||||
function copyResponse<T>(
|
|
||||||
response: HttpResponse<T>,
|
|
||||||
deep: boolean,
|
|
||||||
): HttpResponse<T> {
|
|
||||||
const { body, statusCode, headers } = response;
|
|
||||||
return deep
|
|
||||||
? {
|
|
||||||
statusCode,
|
|
||||||
body: body instanceof Buffer ? (body.subarray() as T) : clone<T>(body),
|
|
||||||
headers: clone(headers),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
statusCode,
|
|
||||||
body,
|
|
||||||
headers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyDefaultHeaders(options: Options): void {
|
function applyDefaultHeaders(options: Options): void {
|
||||||
const renovateVersion = pkg.version;
|
const renovateVersion = pkg.version;
|
||||||
options.headers = {
|
options.headers = {
|
||||||
|
@ -142,13 +124,17 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
||||||
options: HttpOptions = {},
|
options: HttpOptions = {},
|
||||||
) {
|
) {
|
||||||
const retryLimit = process.env.NODE_ENV === 'test' ? 0 : 2;
|
const retryLimit = process.env.NODE_ENV === 'test' ? 0 : 2;
|
||||||
this.options = merge<GotOptions>(options, {
|
this.options = merge<GotOptions>(
|
||||||
context: { hostType },
|
options,
|
||||||
retry: {
|
{
|
||||||
limit: retryLimit,
|
context: { hostType },
|
||||||
maxRetryAfter: 0, // Don't rely on `got` retry-after handling, just let it fail and then we'll handle it
|
retry: {
|
||||||
|
limit: retryLimit,
|
||||||
|
maxRetryAfter: 0, // Don't rely on `got` retry-after handling, just let it fail and then we'll handle it
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
{ isMergeableObject: is.plainObject },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getThrottle(url: string): Throttle | null {
|
protected getThrottle(url: string): Throttle | null {
|
||||||
|
@ -164,13 +150,14 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
||||||
url = resolveBaseUrl(httpOptions.baseUrl, url);
|
url = resolveBaseUrl(httpOptions.baseUrl, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = merge<SetRequired<GotOptions, 'method'>, GotOptions>(
|
let options = merge<SetRequired<GotOptions, 'method'>, InternalHttpOptions>(
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
...this.options,
|
...this.options,
|
||||||
hostType: this.hostType,
|
hostType: this.hostType,
|
||||||
},
|
},
|
||||||
httpOptions,
|
httpOptions,
|
||||||
|
{ isMergeableObject: is.plainObject },
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.trace(`HTTP request: ${options.method.toUpperCase()} ${url}`);
|
logger.trace(`HTTP request: ${options.method.toUpperCase()} ${url}`);
|
||||||
|
@ -212,7 +199,9 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
||||||
// istanbul ignore else: no cache tests
|
// istanbul ignore else: no cache tests
|
||||||
if (!resPromise) {
|
if (!resPromise) {
|
||||||
if (httpOptions.repoCache) {
|
if (httpOptions.repoCache) {
|
||||||
const responseCache = getCache().httpCache?.[url];
|
const responseCache = getCache().httpCache?.[url] as
|
||||||
|
| HttpCache
|
||||||
|
| undefined;
|
||||||
// Prefer If-Modified-Since over If-None-Match
|
// Prefer If-Modified-Since over If-None-Match
|
||||||
if (responseCache?.['lastModified']) {
|
if (responseCache?.['lastModified']) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
@ -232,6 +221,11 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.cacheProvider) {
|
||||||
|
await options.cacheProvider.setCacheHeaders(url, options);
|
||||||
|
}
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const httpTask: GotTask<T> = () => {
|
const httpTask: GotTask<T> = () => {
|
||||||
const queueMs = Date.now() - startTime;
|
const queueMs = Date.now() - startTime;
|
||||||
|
@ -261,6 +255,7 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
||||||
const deepCopyNeeded = !!memCacheKey && res.statusCode !== 304;
|
const deepCopyNeeded = !!memCacheKey && res.statusCode !== 304;
|
||||||
const resCopy = copyResponse(res, deepCopyNeeded);
|
const resCopy = copyResponse(res, deepCopyNeeded);
|
||||||
resCopy.authorization = !!options?.headers?.authorization;
|
resCopy.authorization = !!options?.headers?.authorization;
|
||||||
|
|
||||||
if (httpOptions.repoCache) {
|
if (httpOptions.repoCache) {
|
||||||
const cache = getCache();
|
const cache = getCache();
|
||||||
cache.httpCache ??= {};
|
cache.httpCache ??= {};
|
||||||
|
@ -279,19 +274,25 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
||||||
timeStamp: new Date().toISOString(),
|
timeStamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (resCopy.statusCode === 304 && cache.httpCache[url]?.httpResponse) {
|
const httpCache = cache.httpCache[url] as HttpCache | undefined;
|
||||||
|
if (resCopy.statusCode === 304 && httpCache) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`http cache: Using cached response: ${url} from ${cache.httpCache[url].timeStamp}`,
|
`http cache: Using cached response: ${url} from ${httpCache.timeStamp}`,
|
||||||
);
|
);
|
||||||
HttpCacheStats.incRemoteHits(url);
|
HttpCacheStats.incRemoteHits(url);
|
||||||
const cacheCopy = copyResponse(
|
const cacheCopy = copyResponse(
|
||||||
cache.httpCache[url].httpResponse,
|
httpCache.httpResponse,
|
||||||
deepCopyNeeded,
|
deepCopyNeeded,
|
||||||
);
|
);
|
||||||
cacheCopy.authorization = !!options?.headers?.authorization;
|
cacheCopy.authorization = !!options?.headers?.authorization;
|
||||||
return cacheCopy as HttpResponse<T>;
|
return cacheCopy as HttpResponse<T>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.cacheProvider) {
|
||||||
|
return await options.cacheProvider.wrapResponse(url, resCopy);
|
||||||
|
}
|
||||||
|
|
||||||
return resCopy;
|
return resCopy;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const { abortOnError, abortIgnoreStatusCodes } = options;
|
const { abortOnError, abortIgnoreStatusCodes } = options;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
OptionsOfJSONResponseBody,
|
OptionsOfJSONResponseBody,
|
||||||
ParseJsonFunction,
|
ParseJsonFunction,
|
||||||
} from 'got';
|
} from 'got';
|
||||||
|
import type { HttpCacheProvider } from './cache/types';
|
||||||
|
|
||||||
export type GotContextOptions = {
|
export type GotContextOptions = {
|
||||||
authType?: string;
|
authType?: string;
|
||||||
|
@ -65,7 +66,11 @@ export interface HttpOptions {
|
||||||
|
|
||||||
token?: string;
|
token?: string;
|
||||||
memCache?: boolean;
|
memCache?: boolean;
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
repoCache?: boolean;
|
repoCache?: boolean;
|
||||||
|
cacheProvider?: HttpCacheProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InternalHttpOptions extends HttpOptions {
|
export interface InternalHttpOptions extends HttpOptions {
|
||||||
|
@ -73,6 +78,9 @@ export interface InternalHttpOptions extends HttpOptions {
|
||||||
responseType?: 'json' | 'buffer';
|
responseType?: 'json' | 'buffer';
|
||||||
method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head';
|
method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head';
|
||||||
parseJson?: ParseJsonFunction;
|
parseJson?: ParseJsonFunction;
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
repoCache?: boolean;
|
repoCache?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,3 +97,13 @@ export interface HttpResponse<T = string> {
|
||||||
|
|
||||||
export type Task<T> = () => Promise<T>;
|
export type Task<T> = () => Promise<T>;
|
||||||
export type GotTask<T> = Task<HttpResponse<T>>;
|
export type GotTask<T> = Task<HttpResponse<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
export interface HttpCache {
|
||||||
|
etag?: string;
|
||||||
|
httpResponse: HttpResponse<unknown>;
|
||||||
|
lastModified?: string;
|
||||||
|
timeStamp: string;
|
||||||
|
}
|
||||||
|
|
22
lib/util/http/util.ts
Normal file
22
lib/util/http/util.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { clone } from '../clone';
|
||||||
|
import type { HttpResponse } from './types';
|
||||||
|
|
||||||
|
// Copying will help to avoid circular structure
|
||||||
|
// and mutation of the cached response.
|
||||||
|
export function copyResponse<T>(
|
||||||
|
response: HttpResponse<T>,
|
||||||
|
deep: boolean,
|
||||||
|
): HttpResponse<T> {
|
||||||
|
const { body, statusCode, headers } = response;
|
||||||
|
return deep
|
||||||
|
? {
|
||||||
|
statusCode,
|
||||||
|
body: body instanceof Buffer ? (body.subarray() as T) : clone<T>(body),
|
||||||
|
headers: clone(headers),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
statusCode,
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue