• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

safe-global / safe-client-gateway / 19990057407

06 Dec 2025 02:46PM UTC coverage: 90.189% (+0.006%) from 90.183%
19990057407

Pull #2831

github

gjeanmart
Apply comments from PR
Pull Request #2831: feat(network): add per-request timeout support via NetworkRequest

2861 of 3554 branches covered (80.5%)

Branch coverage included in aggregate %.

7 of 7 new or added lines in 2 files covered. (100.0%)

2 existing lines in 2 files now uncovered.

13777 of 14894 relevant lines covered (92.5%)

632.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.92
/src/datasources/network/network.module.ts
1
import { Global, Module } from '@nestjs/common';
108✔
2
import { IConfigurationService } from '@/config/configuration.service.interface';
108✔
3
import { FetchNetworkService } from '@/datasources/network/fetch.network.service';
108✔
4
import { NetworkService } from '@/datasources/network/network.service.interface';
108✔
5
import { NetworkResponse } from '@/datasources/network/entities/network.response.entity';
6
import {
108✔
7
  NetworkRequestError,
8
  NetworkResponseError,
9
} from '@/datasources/network/entities/network.error.entity';
10
import type { Raw } from '@/validation/entities/raw.entity';
11
import { ILoggingService, LoggingService } from '@/logging/logging.interface';
108✔
12
import { LogType } from '@/domain/common/entities/log-type.entity';
108✔
13
import { hashSha1 } from '@/domain/common/utils/utils';
108✔
14

15
export type FetchClient = <T>(
16
  url: string,
17
  options: RequestInit,
18
  timeout?: number,
19
) => Promise<NetworkResponse<T>>;
20

21
const cache: Record<string, Promise<NetworkResponse<unknown>>> = {};
108✔
22

23
/**
24
 * Use this factory to create a {@link FetchClient} instance
25
 * that can be used to make HTTP requests.
26
 */
27
function fetchClientFactory(
28
  configurationService: IConfigurationService,
29
  loggingService: ILoggingService,
30
): FetchClient {
31
  const cacheInFlightRequests = configurationService.getOrThrow<boolean>(
8✔
32
    'features.cacheInFlightRequests',
33
  );
34
  const requestTimeout = configurationService.getOrThrow<number>(
8✔
35
    'httpClient.requestTimeout',
36
  );
37

38
  const request = createRequestFunction(requestTimeout);
8✔
39

40
  if (!cacheInFlightRequests) {
8✔
41
    return request;
6✔
42
  }
43

44
  return createCachedRequestFunction(request, loggingService);
2✔
45
}
46

47
function createRequestFunction(requestTimeout: number) {
48
  return async <T>(
8✔
49
    url: string,
50
    options: RequestInit,
51
    timeout?: number,
52
  ): Promise<NetworkResponse<T>> => {
53
    let urlObject: URL | null = null;
36✔
54
    let response: Response | null = null;
36✔
55

56
    try {
36✔
57
      urlObject = new URL(url);
36✔
58
      const actualTimeout = timeout ?? requestTimeout;
34✔
59
      const signal = options.signal ?? AbortSignal.timeout(actualTimeout);
34✔
60

61
      response = await fetch(url, {
34✔
62
        ...options,
63
        signal,
64
        keepalive: true,
65
      });
66
    } catch (error) {
67
      throw new NetworkRequestError(urlObject, error);
8✔
68
    }
69

70
    // We validate data so don't need worry about casting `null` response
71
    const data = (await response.json().catch(() => null)) as Raw<T>;
28✔
72

73
    if (!response.ok) {
26✔
74
      throw new NetworkResponseError(urlObject, response, data);
16✔
75
    }
76

77
    return {
10✔
78
      status: response.status,
79
      data,
80
    };
81
  };
82
}
83

84
function createCachedRequestFunction(
85
  request: <T>(
86
    url: string,
87
    options: RequestInit,
88
    timeout?: number,
89
  ) => Promise<NetworkResponse<T>>,
90
  loggingService: ILoggingService,
91
) {
92
  return async <T>(
2✔
93
    url: string,
94
    options: RequestInit,
95
    timeout?: number,
96
  ): Promise<NetworkResponse<T>> => {
97
    const key = getCacheKey(url, options, timeout);
28✔
98
    if (key in cache) {
28✔
99
      loggingService.debug({
6✔
100
        type: LogType.ExternalRequestCacheHit,
101
        url,
102
        key,
103
      });
104
    } else {
105
      loggingService.debug({
22✔
106
        type: LogType.ExternalRequestCacheMiss,
107
        url,
108
        key,
109
      });
110

111
      cache[key] = request(url, options, timeout)
22✔
112
        .catch((err) => {
113
          loggingService.debug({
12✔
114
            type: LogType.ExternalRequestCacheError,
115
            url,
116
            key,
117
          });
118
          throw err;
12✔
119
        })
120
        .finally(() => {
121
          delete cache[key];
22✔
122
        });
123
    }
124

125
    return cache[key];
28✔
126
  };
127
}
128

129
function getCacheKey(
130
  url: string,
131
  requestInit?: RequestInit,
132
  timeout?: number,
133
): string {
134
  if (!requestInit && timeout === undefined) {
28!
UNCOV
135
    return url;
×
136
  }
137

138
  // JSON.stringify does not produce a stable key but initially
139
  // use a naive implementation for testing the implementation
140
  // TODO: Revisit this and use a more stable key
141
  const key = JSON.stringify({ url, ...requestInit, timeout });
28✔
142
  return hashSha1(key);
28✔
143
}
144

145
/**
146
 * A {@link Global} Module which provides HTTP support via {@link NetworkService}
147
 * Feature Modules don't need to import this module directly in order to inject
148
 * the {@link NetworkService}.
149
 *
150
 * This module should be included in the "root" application module
151
 */
152
@Global()
153
@Module({
154
  providers: [
155
    {
156
      provide: 'FetchClient',
157
      useFactory: fetchClientFactory,
158
      inject: [IConfigurationService, LoggingService],
159
    },
160
    { provide: NetworkService, useClass: FetchNetworkService },
161
  ],
162
  exports: [NetworkService],
163
})
164
export class NetworkModule {}
108✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc