diff --git a/lib/util/http/index.spec.ts b/lib/util/http/index.spec.ts index 7e8668154cf204f22f394f6d85e8f0ef5239c7c5..49f9f1aff03ec9cfcc744c9f31a35ada6e329100 100644 --- a/lib/util/http/index.spec.ts +++ b/lib/util/http/index.spec.ts @@ -1,4 +1,4 @@ -import { z } from 'zod'; +import { ZodError, z } from 'zod'; import * as httpMock from '../../../test/http-mock'; import { logger } from '../../../test/util'; import { @@ -10,7 +10,7 @@ import * as hostRules from '../host-rules'; import * as queue from './queue'; import * as throttle from './throttle'; import type { HttpResponse } from './types'; -import { Http } from '.'; +import { Http, HttpError } from '.'; const baseUrl = 'http://renovate.com'; @@ -365,6 +365,47 @@ describe('util/http/index', () => { }); }); + describe('getJsonSafe', () => { + it('uses schema for response body', async () => { + httpMock + .scope('http://example.com') + .get('/') + .reply(200, JSON.stringify({ x: 2, y: 2 })); + + const { val, err } = await http + .getJsonSafe('http://example.com', SomeSchema) + .unwrap(); + + expect(val).toBe('2 + 2 = 4'); + expect(err).toBeUndefined(); + }); + + it('returns schema error result', async () => { + httpMock + .scope('http://example.com') + .get('/') + .reply(200, JSON.stringify({ x: '2', y: '2' })); + + const { val, err } = await http + .getJsonSafe('http://example.com', SomeSchema) + .unwrap(); + + expect(val).toBeUndefined(); + expect(err).toBeInstanceOf(ZodError); + }); + + it('returns error result', async () => { + httpMock.scope('http://example.com').get('/').replyWithError('unknown'); + + const { val, err } = await http + .getJsonSafe('http://example.com', SomeSchema) + .unwrap(); + + expect(val).toBeUndefined(); + expect(err).toBeInstanceOf(HttpError); + }); + }); + describe('postJson', () => { it('uses schema for response body', async () => { httpMock diff --git a/lib/util/http/index.ts b/lib/util/http/index.ts index ffaadfcf40f42fb4db98d26eb8d4a0abe64a052f..1846a64902de32bb87a0b6c7952fa1660811b195 100644 --- a/lib/util/http/index.ts +++ b/lib/util/http/index.ts @@ -2,13 +2,14 @@ import merge from 'deepmerge'; import got, { Options, RequestError } from 'got'; import hasha from 'hasha'; import type { SetRequired } from 'type-fest'; -import { infer as Infer, ZodType } from 'zod'; +import { infer as Infer, type ZodError, ZodType } from 'zod'; import { HOST_DISABLED } from '../../constants/error-messages'; import { pkg } from '../../expose.cjs'; import { logger } from '../../logger'; import { ExternalHostError } from '../../types/errors/external-host-error'; import * as memCache from '../cache/memory'; import { clone } from '../clone'; +import { type AsyncResult, Result } from '../result'; import { resolveBaseUrl } from '../url'; import { applyAuthorization, removeAuthorization } from './auth'; import { hooks } from './hooks'; @@ -29,6 +30,9 @@ import './legacy'; export { RequestError as HttpError }; +export class EmptyResultError extends Error {} +export type SafeJsonError = RequestError | ZodError | EmptyResultError; + type JsonArgs< Opts extends HttpOptions & HttpRequestOptions<ResT>, ResT = unknown, @@ -348,6 +352,32 @@ export class Http<Opts extends HttpOptions = HttpOptions> { return this.requestJson<ResT>('get', args); } + getJsonSafe< + ResT extends NonNullable<unknown>, + Schema extends ZodType<ResT> = ZodType<ResT> + >(url: string, schema: Schema): AsyncResult<Infer<Schema>, SafeJsonError>; + getJsonSafe< + ResT extends NonNullable<unknown>, + Schema extends ZodType<ResT> = ZodType<ResT> + >( + url: string, + options: Opts & HttpRequestOptions<Infer<Schema>>, + schema: Schema + ): AsyncResult<Infer<Schema>, SafeJsonError>; + getJsonSafe< + ResT extends NonNullable<unknown>, + Schema extends ZodType<ResT> = ZodType<ResT> + >( + arg1: string, + arg2?: (Opts & HttpRequestOptions<ResT>) | Schema, + arg3?: Schema + ): AsyncResult<ResT, SafeJsonError> { + const args = this.resolveArgs<ResT>(arg1, arg2, arg3); + return Result.wrap(this.requestJson<ResT>('get', args)).transform( + (response) => response.body + ); + } + headJson(url: string, httpOptions?: Opts): Promise<HttpResponse<never>> { return this.requestJson<never>('head', { url, httpOptions }); }