From 030b1a61a45a98557ddfe22732ffa1807619cba5 Mon Sep 17 00:00:00 2001
From: Sergei Zharinov <zharinov@users.noreply.github.com>
Date: Mon, 10 Jan 2022 00:22:27 +0300
Subject: [PATCH] refactor(util/http): Strict null checks for http utils
 (#13416)

* refactor(util/http): Strict null checks for http utils

* More tests for queue.ts

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 lib/util/http/auth.ts             |  31 ++++--
 lib/util/http/bitbucket-server.ts |   2 +-
 lib/util/http/bitbucket.ts        |   7 +-
 lib/util/http/gitea.ts            |  22 ++--
 lib/util/http/github.spec.ts      |   6 +-
 lib/util/http/github.ts           | 175 ++++++++++++++++--------------
 lib/util/http/gitlab.ts           |  22 ++--
 lib/util/http/host-rules.ts       |  18 ++-
 lib/util/http/index.spec.ts       |  38 +++++--
 lib/util/http/index.ts            |  12 +-
 tsconfig.strict.json              |   4 +-
 11 files changed, 193 insertions(+), 144 deletions(-)

diff --git a/lib/util/http/auth.ts b/lib/util/http/auth.ts
index cd182af48d..e083970ca2 100644
--- a/lib/util/http/auth.ts
+++ b/lib/util/http/auth.ts
@@ -1,5 +1,5 @@
 import is from '@sindresorhus/is';
-import type { NormalizedOptions } from 'got';
+import type { Options } from 'got';
 import {
   GITHUB_API_USING_HOST_TYPES,
   GITLAB_API_USING_HOST_TYPES,
@@ -14,10 +14,14 @@ export function applyAuthorization(inOptions: GotOptions): GotOptions {
     return options;
   }
 
+  options.headers ??= {};
   if (options.token) {
     if (options.hostType === PlatformId.Gitea) {
       options.headers.authorization = `token ${options.token}`;
-    } else if (GITHUB_API_USING_HOST_TYPES.includes(options.hostType)) {
+    } else if (
+      options.hostType &&
+      GITHUB_API_USING_HOST_TYPES.includes(options.hostType)
+    ) {
       options.headers.authorization = `token ${options.token}`;
       if (options.token.startsWith('x-access-token:')) {
         const appToken = options.token.replace('x-access-token:', '');
@@ -29,7 +33,10 @@ export function applyAuthorization(inOptions: GotOptions): GotOptions {
           );
         }
       }
-    } else if (GITLAB_API_USING_HOST_TYPES.includes(options.hostType)) {
+    } else if (
+      options.hostType &&
+      GITLAB_API_USING_HOST_TYPES.includes(options.hostType)
+    ) {
       // GitLab versions earlier than 12.2 only support authentication with
       // a personal access token, which is 20 characters long.
       if (options.token.length === 20) {
@@ -51,7 +58,7 @@ export function applyAuthorization(inOptions: GotOptions): GotOptions {
   } else if (options.password !== undefined) {
     // Otherwise got will add username and password to url and header
     const auth = Buffer.from(
-      `${options.username || ''}:${options.password}`
+      `${options.username ?? ''}:${options.password}`
     ).toString('base64');
     options.headers.authorization = `Basic ${auth}`;
     delete options.username;
@@ -61,20 +68,20 @@ export function applyAuthorization(inOptions: GotOptions): GotOptions {
 }
 
 // isAmazon return true if request options contains Amazon related headers
-function isAmazon(options: NormalizedOptions): boolean {
-  return options.search?.includes('X-Amz-Algorithm');
+function isAmazon(options: Options): boolean {
+  return !!options.search?.includes('X-Amz-Algorithm');
 }
 
 // isAzureBlob return true if request options contains Azure container registry related data
-function isAzureBlob(options: NormalizedOptions): boolean {
-  return (
+function isAzureBlob(options: Options): boolean {
+  return !!(
     options.hostname?.endsWith('.blob.core.windows.net') && // lgtm [js/incomplete-url-substring-sanitization]
     options.href?.includes('/docker/registry')
   );
 }
 
 // removeAuthorization from the redirect options
-export function removeAuthorization(options: NormalizedOptions): void {
+export function removeAuthorization(options: Options): void {
   if (!options.password && !options.headers?.authorization) {
     return;
   }
@@ -83,7 +90,7 @@ export function removeAuthorization(options: NormalizedOptions): void {
   if (isAmazon(options) || isAzureBlob(options)) {
     // if there is no port in the redirect URL string, then delete it from the redirect options.
     // This can be evaluated for removal after upgrading to Got v10
-    const portInUrl = options.href.split('/')[2].split(':')[1];
+    const portInUrl = options.href?.split?.('/')?.[2]?.split(':')?.[1];
     // istanbul ignore next
     if (!portInUrl) {
       delete options.port; // Redirect will instead use 80 or 443 for HTTP or HTTPS respectively
@@ -91,7 +98,9 @@ export function removeAuthorization(options: NormalizedOptions): void {
 
     // registry is hosted on Amazon or Azure blob, redirect url includes
     // authentication which is not required and should be removed
-    delete options.headers.authorization;
+    if (options?.headers?.authorization) {
+      delete options.headers.authorization;
+    }
     delete options.username;
     delete options.password;
   }
diff --git a/lib/util/http/bitbucket-server.ts b/lib/util/http/bitbucket-server.ts
index 6d2d91aa3e..dc3d8eaa0a 100644
--- a/lib/util/http/bitbucket-server.ts
+++ b/lib/util/http/bitbucket-server.ts
@@ -15,7 +15,7 @@ export class BitbucketServerHttp extends Http {
   protected override request<T>(
     path: string,
     options?: InternalHttpOptions
-  ): Promise<HttpResponse<T> | null> {
+  ): Promise<HttpResponse<T>> {
     const url = resolveBaseUrl(baseUrl, path);
     const opts = {
       baseUrl,
diff --git a/lib/util/http/bitbucket.ts b/lib/util/http/bitbucket.ts
index 1eaaab9e53..1445d0e847 100644
--- a/lib/util/http/bitbucket.ts
+++ b/lib/util/http/bitbucket.ts
@@ -15,11 +15,8 @@ export class BitbucketHttp extends Http {
   protected override request<T>(
     url: string | URL,
     options?: InternalHttpOptions
-  ): Promise<HttpResponse<T> | null> {
-    const opts = {
-      baseUrl,
-      ...options,
-    };
+  ): Promise<HttpResponse<T>> {
+    const opts = { baseUrl, ...options };
     return super.request<T>(url, opts);
   }
 }
diff --git a/lib/util/http/gitea.ts b/lib/util/http/gitea.ts
index 110589be7d..dc154fb99e 100644
--- a/lib/util/http/gitea.ts
+++ b/lib/util/http/gitea.ts
@@ -1,3 +1,4 @@
+import is from '@sindresorhus/is';
 import { PlatformId } from '../../constants';
 import { resolveBaseUrl } from '../url';
 import { Http, HttpOptions, HttpResponse, InternalHttpOptions } from '.';
@@ -12,12 +13,13 @@ export interface GiteaHttpOptions extends InternalHttpOptions {
   token?: string;
 }
 
-function getPaginationContainer(body: any): any[] {
-  if (Array.isArray(body) && body.length) {
-    return body;
+function getPaginationContainer<T = unknown>(body: unknown): T[] | null {
+  if (is.array(body) && body.length) {
+    return body as T[];
   }
-  if (Array.isArray(body?.data) && body.data.length) {
-    return body.data;
+
+  if (is.plainObject(body) && is.array(body?.data) && body.data.length) {
+    return body.data as T[];
   }
 
   return null;
@@ -36,24 +38,24 @@ export class GiteaHttp extends Http<GiteaHttpOptions, GiteaHttpOptions> {
   protected override async request<T>(
     path: string,
     options?: InternalHttpOptions & GiteaHttpOptions
-  ): Promise<HttpResponse<T> | null> {
-    const resolvedUrl = resolveUrl(path, options.baseUrl ?? baseUrl);
+  ): Promise<HttpResponse<T>> {
+    const resolvedUrl = resolveUrl(path, options?.baseUrl ?? baseUrl);
     const opts = {
       baseUrl,
       ...options,
     };
     const res = await super.request<T>(resolvedUrl, opts);
-    const pc = getPaginationContainer(res.body);
+    const pc = getPaginationContainer<T>(res.body);
     if (opts.paginate && pc) {
       const total = parseInt(res.headers['x-total-count'] as string, 10);
-      let nextPage = parseInt(resolvedUrl.searchParams.get('page') || '1', 10);
+      let nextPage = parseInt(resolvedUrl.searchParams.get('page') ?? '1', 10);
 
       while (total && pc.length < total) {
         nextPage += 1;
         resolvedUrl.searchParams.set('page', nextPage.toString());
 
         const nextRes = await super.request<T>(resolvedUrl.toString(), opts);
-        const nextPc = getPaginationContainer(nextRes.body);
+        const nextPc = getPaginationContainer<T>(nextRes.body);
         if (nextPc === null) {
           break;
         }
diff --git a/lib/util/http/github.spec.ts b/lib/util/http/github.spec.ts
index abd17fc506..26d2714252 100644
--- a/lib/util/http/github.spec.ts
+++ b/lib/util/http/github.spec.ts
@@ -146,7 +146,7 @@ describe('util/http/github', () => {
       async function fail(
         code: number,
         body: any = undefined,
-        headers: httpMock.ReplyHeaders = undefined
+        headers: httpMock.ReplyHeaders = {}
       ) {
         const url = '/some-url';
         httpMock
@@ -441,9 +441,9 @@ describe('util/http/github', () => {
         .post('/graphql')
         .reply(200, { data: { repository } });
 
-      const { data } = await githubApi.requestGraphql(graphqlQuery);
+      const res = await githubApi.requestGraphql(graphqlQuery);
       expect(httpMock.getTrace()).toHaveLength(1);
-      expect(data).toStrictEqual({ repository });
+      expect(res?.data).toStrictEqual({ repository });
     });
     it('queryRepoField', async () => {
       httpMock
diff --git a/lib/util/http/github.ts b/lib/util/http/github.ts
index fa4bfbe04a..ec0e949a68 100644
--- a/lib/util/http/github.ts
+++ b/lib/util/http/github.ts
@@ -49,11 +49,12 @@ function handleGotError(
   err: GotLegacyError,
   url: string | URL,
   opts: GithubHttpOptions
-): never {
+): Error {
   const path = url.toString();
   let message = err.message || '';
-  if (is.plainObject(err.response?.body) && 'message' in err.response.body) {
-    message = String(err.response.body.message);
+  const body = err.response?.body;
+  if (is.plainObject(body) && 'message' in body) {
+    message = String(body.message);
   }
   if (
     err.code === 'ENOTFOUND' ||
@@ -62,37 +63,37 @@ function handleGotError(
     err.code === 'ECONNRESET'
   ) {
     logger.debug({ err }, 'GitHub failure: RequestError');
-    throw new ExternalHostError(err, PlatformId.Github);
+    return new ExternalHostError(err, PlatformId.Github);
   }
   if (err.name === 'ParseError') {
     logger.debug({ err }, '');
-    throw new ExternalHostError(err, PlatformId.Github);
+    return new ExternalHostError(err, PlatformId.Github);
   }
-  if (err.statusCode >= 500 && err.statusCode < 600) {
+  if (err.statusCode && err.statusCode >= 500 && err.statusCode < 600) {
     logger.debug({ err }, 'GitHub failure: 5xx');
-    throw new ExternalHostError(err, PlatformId.Github);
+    return new ExternalHostError(err, PlatformId.Github);
   }
   if (
     err.statusCode === 403 &&
     message.startsWith('You have triggered an abuse detection mechanism')
   ) {
     logger.debug({ err }, 'GitHub failure: abuse detection');
-    throw new Error(PLATFORM_RATE_LIMIT_EXCEEDED);
+    return new Error(PLATFORM_RATE_LIMIT_EXCEEDED);
   }
   if (
     err.statusCode === 403 &&
     message.startsWith('You have exceeded a secondary rate limit')
   ) {
     logger.debug({ err }, 'GitHub failure: secondary rate limit');
-    throw new Error(PLATFORM_RATE_LIMIT_EXCEEDED);
+    return new Error(PLATFORM_RATE_LIMIT_EXCEEDED);
   }
   if (err.statusCode === 403 && message.includes('Upgrade to GitHub Pro')) {
     logger.debug({ path }, 'Endpoint needs paid GitHub plan');
-    throw err;
+    return err;
   }
   if (err.statusCode === 403 && message.includes('rate limit exceeded')) {
     logger.debug({ err }, 'GitHub failure: rate limit');
-    throw new Error(PLATFORM_RATE_LIMIT_EXCEEDED);
+    return new Error(PLATFORM_RATE_LIMIT_EXCEEDED);
   }
   if (
     err.statusCode === 403 &&
@@ -102,7 +103,7 @@ function handleGotError(
       { err },
       'GitHub failure: Resource not accessible by integration'
     );
-    throw new Error(PLATFORM_INTEGRATION_UNAUTHORIZED);
+    return new Error(PLATFORM_INTEGRATION_UNAUTHORIZED);
   }
   if (err.statusCode === 401 && message.includes('Bad credentials')) {
     const rateLimit = err.headers?.['x-ratelimit-limit'] ?? -1;
@@ -114,40 +115,40 @@ function handleGotError(
       'GitHub failure: Bad credentials'
     );
     if (rateLimit === '60') {
-      throw new ExternalHostError(err, PlatformId.Github);
+      return new ExternalHostError(err, PlatformId.Github);
     }
-    throw new Error(PLATFORM_BAD_CREDENTIALS);
+    return new Error(PLATFORM_BAD_CREDENTIALS);
   }
   if (err.statusCode === 422) {
     if (
       message.includes('Review cannot be requested from pull request author')
     ) {
-      throw err;
+      return err;
     } else if (err.body?.errors?.find((e: any) => e.code === 'invalid')) {
       logger.debug({ err }, 'Received invalid response - aborting');
-      throw new Error(REPOSITORY_CHANGED);
+      return new Error(REPOSITORY_CHANGED);
     } else if (
       err.body?.errors?.find((e: any) =>
         e.message?.startsWith('A pull request already exists')
       )
     ) {
-      throw err;
+      return err;
     }
     logger.debug({ err }, '422 Error thrown from GitHub');
-    throw new ExternalHostError(err, PlatformId.Github);
+    return new ExternalHostError(err, PlatformId.Github);
   }
   if (
     err.statusCode === 410 &&
     err.body?.message === 'Issues are disabled for this repo'
   ) {
-    throw err;
+    return err;
   }
   if (err.statusCode === 404) {
     logger.debug({ url: path }, 'GitHub 404');
   } else {
     logger.debug({ err }, 'Unknown GitHub error');
   }
-  throw err;
+  return err;
 }
 
 interface GraphqlOptions {
@@ -155,10 +156,16 @@ interface GraphqlOptions {
   paginate?: boolean;
   count?: number;
   limit?: number;
-  cursor?: string;
+  cursor?: string | null;
   acceptHeader?: string;
 }
 
+interface GraphqlPaginatedContent<T = unknown> {
+  nodes: T[];
+  edges: T[];
+  pageInfo: { hasNextPage: boolean; endCursor: string };
+}
+
 function constructAcceptString(input?: any): string {
   const defaultAccept = 'application/vnd.github.v3+json';
   const acceptStrings =
@@ -184,9 +191,7 @@ export class GithubHttp extends Http<GithubHttpOptions, GithubHttpOptions> {
     url: string | URL,
     options?: GithubInternalOptions & GithubHttpOptions,
     okToRetry = true
-  ): Promise<HttpResponse<T> | null> {
-    let result = null;
-
+  ): Promise<HttpResponse<T>> {
     const opts = {
       baseUrl,
       ...options,
@@ -201,68 +206,67 @@ export class GithubHttp extends Http<GithubHttpOptions, GithubHttpOptions> {
     };
 
     try {
-      result = await super.request<T>(url, opts);
-
-      // istanbul ignore else: Can result be null ???
-      if (result !== null) {
-        if (opts.paginate) {
-          // Check if result is paginated
-          const pageLimit = opts.pageLimit || 10;
-          const linkHeader =
-            result?.headers?.link &&
-            parseLinkHeader(result.headers.link as string);
-          if (linkHeader?.next && linkHeader?.last) {
-            let lastPage = +linkHeader.last.page;
-            // istanbul ignore else: needs a test
-            if (!process.env.RENOVATE_PAGINATE_ALL && opts.paginate !== 'all') {
-              lastPage = Math.min(pageLimit, lastPage);
+      const result = await super.request<T>(url, opts);
+      if (opts.paginate) {
+        // Check if result is paginated
+        const pageLimit = opts.pageLimit ?? 10;
+        const linkHeader =
+          result?.headers?.link &&
+          parseLinkHeader(result.headers.link as string);
+        if (linkHeader?.next && linkHeader?.last) {
+          let lastPage = +linkHeader.last.page;
+          // istanbul ignore else: needs a test
+          if (!process.env.RENOVATE_PAGINATE_ALL && opts.paginate !== 'all') {
+            lastPage = Math.min(pageLimit, lastPage);
+          }
+          const pageNumbers = Array.from(
+            new Array(lastPage),
+            (x, i) => i + 1
+          ).slice(1);
+          const queue = pageNumbers.map(
+            (page) => (): Promise<HttpResponse<T>> => {
+              const nextUrl = new URL(linkHeader.next.url, baseUrl);
+              nextUrl.search = '';
+              nextUrl.searchParams.set('page', page.toString());
+              return this.request<T>(
+                nextUrl,
+                { ...opts, paginate: false },
+                okToRetry
+              );
             }
-            const pageNumbers = Array.from(
-              new Array(lastPage),
-              (x, i) => i + 1
-            ).slice(1);
-            const queue = pageNumbers.map(
-              (page) => (): Promise<HttpResponse> => {
-                const nextUrl = new URL(linkHeader.next.url, baseUrl);
-                delete nextUrl.search;
-                nextUrl.searchParams.set('page', page.toString());
-                return this.request(
-                  nextUrl,
-                  { ...opts, paginate: false },
-                  okToRetry
-                );
+          );
+          const pages = await pAll(queue, { concurrency: 5 });
+          if (opts.paginationField && is.plainObject(result.body)) {
+            const paginatedResult = result.body[opts.paginationField];
+            if (is.array<T>(paginatedResult)) {
+              for (const nextPage of pages) {
+                if (is.plainObject(nextPage.body)) {
+                  const nextPageResults = nextPage.body[opts.paginationField];
+                  if (is.array<T>(nextPageResults)) {
+                    paginatedResult.push(...nextPageResults);
+                  }
+                }
+              }
+            }
+          } else if (is.array<T>(result.body)) {
+            for (const nextPage of pages) {
+              if (is.array<T>(nextPage.body)) {
+                result.body.push(...nextPage.body);
               }
-            );
-            const pages = await pAll(queue, { concurrency: 5 });
-            if (opts.paginationField) {
-              result.body[opts.paginationField] = result.body[
-                opts.paginationField
-              ].concat(
-                ...pages
-                  .filter(Boolean)
-                  .map((page) => page.body[opts.paginationField])
-              );
-            } else {
-              result.body = result.body.concat(
-                ...pages.filter(Boolean).map((page) => page.body)
-              );
             }
           }
         }
       }
+      return result;
     } catch (err) {
-      handleGotError(err, url, opts);
+      throw handleGotError(err, url, opts);
     }
-
-    return result;
   }
 
   public async requestGraphql<T = unknown>(
     query: string,
     options: GraphqlOptions = {}
-  ): Promise<GithubGraphqlResponse<T>> {
-    let result = null;
-
+  ): Promise<GithubGraphqlResponse<T> | null> {
     const path = 'graphql';
 
     const { paginate, count = 100, cursor = null } = options;
@@ -289,16 +293,15 @@ export class GithubHttp extends Http<GithubHttpOptions, GithubHttpOptions> {
         'graphql',
         opts
       );
-      result = res?.body;
+      return res?.body;
     } catch (err) {
       logger.debug({ err, query, options }, 'Unexpected GraphQL Error');
       if (err instanceof ExternalHostError && count && count > 10) {
         logger.info('Reducing pagination count to workaround graphql errors');
         return null;
       }
-      handleGotError(err, path, opts);
+      throw handleGotError(err, path, opts);
     }
-    return result;
   }
 
   async queryRepoField<T = Record<string, unknown>>(
@@ -311,10 +314,10 @@ export class GithubHttp extends Http<GithubHttpOptions, GithubHttpOptions> {
     const { paginate = true } = options;
 
     let optimalCount: null | number = null;
-    const initialCount = options.count || 100;
+    const initialCount = options.count ?? 100;
     let count = initialCount;
-    let limit = options.limit || 1000;
-    let cursor: string = null;
+    let limit = options.limit ?? 1000;
+    let cursor: string | null = null;
 
     let isIterating = true;
     while (isIterating) {
@@ -324,11 +327,19 @@ export class GithubHttp extends Http<GithubHttpOptions, GithubHttpOptions> {
         cursor,
         paginate,
       });
-      const fieldData = res?.data?.repository?.[fieldName];
-      if (fieldData) {
+      const repositoryData = res?.data?.repository;
+      if (
+        repositoryData &&
+        is.plainObject(repositoryData) &&
+        repositoryData[fieldName]
+      ) {
         optimalCount = count;
 
-        const { nodes = [], edges = [], pageInfo } = fieldData;
+        const {
+          nodes = [],
+          edges = [],
+          pageInfo,
+        } = repositoryData[fieldName] as GraphqlPaginatedContent<T>;
         result.push(...nodes);
         result.push(...edges);
 
diff --git a/lib/util/http/gitlab.ts b/lib/util/http/gitlab.ts
index 7c6589ce98..538837d2f1 100644
--- a/lib/util/http/gitlab.ts
+++ b/lib/util/http/gitlab.ts
@@ -1,3 +1,4 @@
+import is from '@sindresorhus/is';
 import parseLinkHeader from 'parse-link-header';
 import { PlatformId } from '../../constants';
 import { logger } from '../../logger';
@@ -27,9 +28,7 @@ export class GitlabHttp extends Http<GitlabHttpOptions, GitlabHttpOptions> {
   protected override async request<T>(
     url: string | URL,
     options?: GitlabInternalOptions & GitlabHttpOptions
-  ): Promise<HttpResponse<T> | null> {
-    let result = null;
-
+  ): Promise<HttpResponse<T>> {
     const opts = {
       baseUrl,
       ...options,
@@ -37,22 +36,25 @@ export class GitlabHttp extends Http<GitlabHttpOptions, GitlabHttpOptions> {
     };
 
     try {
-      result = await super.request<T>(url, opts);
-      if (opts.paginate) {
+      const result = await super.request<T>(url, opts);
+      if (opts.paginate && is.array(result.body)) {
         // Check if result is paginated
         try {
           const linkHeader = parseLinkHeader(result.headers.link as string);
-          if (linkHeader?.next) {
-            const nextUrl = parseUrl(linkHeader.next.url);
+          const nextUrl = linkHeader?.next?.url
+            ? parseUrl(linkHeader.next.url)
+            : null;
+          if (nextUrl) {
             if (process.env.GITLAB_IGNORE_REPO_URL) {
               const defaultEndpoint = new URL(baseUrl);
               nextUrl.protocol = defaultEndpoint.protocol;
               nextUrl.host = defaultEndpoint.host;
             }
 
-            result.body = result.body.concat(
-              (await this.request<T>(nextUrl, opts)).body
-            );
+            const nextResult = await this.request<T>(nextUrl, opts);
+            if (is.array(nextResult.body)) {
+              result.body.push(...nextResult.body);
+            }
           }
         } catch (err) /* istanbul ignore next */ {
           logger.warn({ err }, 'Pagination error');
diff --git a/lib/util/http/host-rules.ts b/lib/util/http/host-rules.ts
index 4d86c913d6..3c786d1709 100644
--- a/lib/util/http/host-rules.ts
+++ b/lib/util/http/host-rules.ts
@@ -20,6 +20,7 @@ function findMatchingRules(options: GotOptions, url: string): HostRule {
 
   // Fallback to `github` hostType
   if (
+    hostType &&
     GITHUB_API_USING_HOST_TYPES.includes(hostType) &&
     hostType !== PlatformId.Github
   ) {
@@ -34,6 +35,7 @@ function findMatchingRules(options: GotOptions, url: string): HostRule {
 
   // Fallback to `gitlab` hostType
   if (
+    hostType &&
     GITLAB_API_USING_HOST_TYPES.includes(hostType) &&
     hostType !== PlatformId.Gitlab
   ) {
@@ -74,11 +76,17 @@ export function applyHostRules(url: string, inOptions: GotOptions): GotOptions {
     options.enabled = false;
   }
   // Apply optional params
-  ['abortOnError', 'abortIgnoreStatusCodes', 'timeout'].forEach((param) => {
-    if (foundRules[param]) {
-      options[param] = foundRules[param];
-    }
-  });
+  if (foundRules.abortOnError) {
+    options.abortOnError = foundRules.abortOnError;
+  }
+
+  if (foundRules.abortIgnoreStatusCodes) {
+    options.abortIgnoreStatusCodes = foundRules.abortIgnoreStatusCodes;
+  }
+
+  if (foundRules.timeout) {
+    options.timeout = foundRules.timeout;
+  }
 
   if (!hasProxy() && foundRules.enableHttp2 === true) {
     options.http2 = true;
diff --git a/lib/util/http/index.spec.ts b/lib/util/http/index.spec.ts
index c239376549..04b1f54f03 100644
--- a/lib/util/http/index.spec.ts
+++ b/lib/util/http/index.spec.ts
@@ -192,22 +192,44 @@ describe('util/http/index', () => {
     let bar = false;
     let baz = false;
 
-    const mockRequestResponse = () => {
-      let resolveRequest;
+    const dummyResolve = (_: unknown): void => {
+      return;
+    };
+
+    interface MockedRequestResponse<T = unknown> {
+      request: Promise<T>;
+      resolveRequest: (_?: T) => void;
+      response: Promise<T>;
+      resolveResponse: (_?: T) => void;
+    }
+
+    const mockRequestResponse = (): MockedRequestResponse => {
+      let resolveRequest = dummyResolve;
       const request = new Promise((resolve) => {
         resolveRequest = resolve;
       });
 
-      let resolveResponse;
+      let resolveResponse = dummyResolve;
       const response = new Promise((resolve) => {
         resolveResponse = resolve;
       });
 
-      return [request, resolveRequest, response, resolveResponse];
+      return { request, resolveRequest, response, resolveResponse };
     };
 
-    const [fooReq, fooStart, fooResp, fooFinish] = mockRequestResponse();
-    const [barReq, barStart, barResp, barFinish] = mockRequestResponse();
+    const {
+      request: fooReq,
+      resolveRequest: fooStart,
+      response: fooResp,
+      resolveResponse: fooFinish,
+    } = mockRequestResponse();
+
+    const {
+      request: barReq,
+      resolveRequest: barStart,
+      response: barResp,
+      resolveResponse: barFinish,
+    } = mockRequestResponse();
 
     httpMock
       .scope(baseUrl)
@@ -256,7 +278,7 @@ describe('util/http/index', () => {
   it('getBuffer', async () => {
     httpMock.scope(baseUrl).get('/').reply(200, Buffer.from('test'));
     const res = await http.getBuffer('http://renovate.com');
-    expect(res.body).toBeInstanceOf(Buffer);
-    expect(res.body.toString('utf-8')).toBe('test');
+    expect(res?.body).toBeInstanceOf(Buffer);
+    expect(res?.body.toString('utf-8')).toBe('test');
   });
 });
diff --git a/lib/util/http/index.ts b/lib/util/http/index.ts
index e1a7d00c3e..553e912672 100644
--- a/lib/util/http/index.ts
+++ b/lib/util/http/index.ts
@@ -74,7 +74,7 @@ function applyDefaultHeaders(options: Options): void {
   options.headers = {
     ...options.headers,
     'user-agent':
-      process.env.RENOVATE_USER_AGENT ||
+      process.env.RENOVATE_USER_AGENT ??
       `RenovateBot/${renovateVersion} (https://github.com/renovatebot/renovate)`,
   };
 }
@@ -95,7 +95,7 @@ async function gotRoutine<T>(
   // Otherwise it doesn't typecheck.
   const resp = await got<T>(url, { ...options, hooks } as GotJSONOptions);
   const duration =
-    resp.timings.phases.total || /* istanbul ignore next: can't be tested */ 0;
+    resp.timings.phases.total ?? /* istanbul ignore next: can't be tested */ 0;
 
   const httpRequests = memCache.get('http-requests') || [];
   httpRequests.push({ ...requestStats, duration });
@@ -107,14 +107,14 @@ async function gotRoutine<T>(
 export class Http<GetOptions = HttpOptions, PostOptions = HttpPostOptions> {
   private options?: GotOptions;
 
-  constructor(private hostType: string, options?: HttpOptions) {
+  constructor(private hostType: string, options: HttpOptions = {}) {
     this.options = merge<GotOptions>(options, { context: { hostType } });
   }
 
   protected async request<T>(
     requestUrl: string | URL,
-    httpOptions?: InternalHttpOptions
-  ): Promise<HttpResponse<T> | null> {
+    httpOptions: InternalHttpOptions = {}
+  ): Promise<HttpResponse<T>> {
     let url = requestUrl.toString();
     if (httpOptions?.baseUrl) {
       url = resolveBaseUrl(httpOptions.baseUrl, url);
@@ -160,7 +160,7 @@ export class Http<GetOptions = HttpOptions, PostOptions = HttpPostOptions> {
 
     // Cache GET requests unless useCache=false
     if (
-      ['get', 'head'].includes(options.method) &&
+      (options.method === 'get' || options.method === 'head') &&
       options.useCache !== false
     ) {
       resPromise = memCache.get(cacheKey);
diff --git a/tsconfig.strict.json b/tsconfig.strict.json
index 2a9c713c0b..abbedf333e 100644
--- a/tsconfig.strict.json
+++ b/tsconfig.strict.json
@@ -50,9 +50,7 @@
     "lib/util/git/types.ts",
     "lib/util/host-rules.ts",
     "lib/util/html.ts",
-    "lib/util/http/hooks.ts",
-    "lib/util/http/legacy.ts",
-    "lib/util/http/types.ts",
+    "lib/util/http/**/.ts",
     "lib/util/index.ts",
     "lib/util/json-writer/code-format.ts",
     "lib/util/json-writer/editor-config.ts",
-- 
GitLab