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

safe-global / safe-client-gateway / 20109826154

10 Dec 2025 06:52PM UTC coverage: 88.831% (-0.2%) from 89.036%
20109826154

Pull #2803

github

PooyaRaki
refactor(circuit-breaker): remove global circuit breaker configuration and related code
Pull Request #2803: feat(circuit-breaker): implement circuit breaker pattern for resilience

2872 of 3615 branches covered (79.45%)

Branch coverage included in aggregate %.

79 of 105 new or added lines in 6 files covered. (75.24%)

3 existing lines in 3 files now uncovered.

13687 of 15026 relevant lines covered (91.09%)

591.5 hits per line

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

76.62
/src/datasources/network/network.module.ts
1
import { Global, Module } from '@nestjs/common';
94✔
2
import { IConfigurationService } from '@/config/configuration.service.interface';
94✔
3
import { FetchNetworkService } from '@/datasources/network/fetch.network.service';
94✔
4
import { NetworkService } from '@/datasources/network/network.service.interface';
94✔
5
import { NetworkResponse } from '@/datasources/network/entities/network.response.entity';
6
import {
94✔
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';
94✔
12
import { LogType } from '@/domain/common/entities/log-type.entity';
94✔
13
import { hashSha1 } from '@/domain/common/utils/utils';
94✔
14
import { CircuitBreakerService } from '@/datasources/circuit-breaker/circuit-breaker.service';
94✔
15
import { CircuitBreakerException } from '@/datasources/circuit-breaker/exceptions/circuit-breaker.exception';
94✔
16

17
export type FetchClient = <T>(
18
  url: string,
19
  options: RequestInit,
20
  timeout?: number,
21
  useCircuitBreaker?: boolean,
22
) => Promise<NetworkResponse<T>>;
23

24
const cache: Record<string, Promise<NetworkResponse<unknown>>> = {};
94✔
25

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

42
  const baseRequest = createRequestFunction(defaultTimeout);
4✔
43
  const circuitBreakerRequest = createCircuitBreakerRequestFunction(
4✔
44
    baseRequest,
45
    circuitBreakerService,
46
  );
47

48
  if (!cacheInFlightRequests) {
4✔
49
    return circuitBreakerRequest;
2✔
50
  }
51

52
  return createCachedRequestFunction(circuitBreakerRequest, loggingService);
2✔
53
}
54

55
function createRequestFunction(defaultTimeout: number) {
56
  return async <T>(
4✔
57
    url: string,
58
    options: RequestInit,
59
    customTimeout?: number,
60
  ): Promise<NetworkResponse<T>> => {
61
    let urlObject: URL | null = null;
34✔
62
    let response: Response | null = null;
34✔
63

64
    try {
34✔
65
      urlObject = new URL(url);
34✔
66
      const timeout = customTimeout ?? defaultTimeout;
32✔
67

68
      response = await fetch(url, {
32✔
69
        ...options,
70
        signal: AbortSignal.timeout(timeout),
71
        keepalive: true,
72
      });
73
    } catch (error) {
74
      throw new NetworkRequestError(urlObject, error);
8✔
75
    }
76

77
    // We validate data so don't need worry about casting `null` response
78
    const data = (await response.json().catch(() => null)) as Raw<T>;
26✔
79

80
    if (!response.ok) {
24✔
81
      throw new NetworkResponseError(urlObject, response, data);
14✔
82
    }
83

84
    return {
10✔
85
      status: response.status,
86
      data,
87
    };
88
  };
89
}
90

91
/**
92
 * Wraps a request function with circuit breaker logic
93
 *
94
 * This function intercepts requests and applies circuit breaker protection:
95
 * - Checks if the circuit is open before allowing the request
96
 * - Records successes and failures based on response status
97
 * - Must be explicitly enabled per request via the useCircuitBreaker parameter
98
 *
99
 * @param request - The base request function to wrap
100
 * @param circuitBreakerService - Service managing circuit breaker state
101
 *
102
 * @returns Wrapped request function with circuit breaker logic
103
 */
104
function createCircuitBreakerRequestFunction(
105
  request: <T>(
106
    url: string,
107
    options: RequestInit,
108
    timeout?: number,
109
  ) => Promise<NetworkResponse<T>>,
110
  circuitBreakerService: CircuitBreakerService,
111
) {
112
  return async <T>(
4✔
113
    url: string,
114
    options: RequestInit,
115
    timeout?: number,
116
    useCircuitBreaker?: boolean,
117
  ): Promise<NetworkResponse<T>> => {
118
    if (!useCircuitBreaker) {
34✔
119
      return request(url, options, timeout);
34✔
120
    }
121

NEW
122
    circuitBreakerService.canProceedOrFail(url);
×
123

NEW
124
    try {
×
NEW
125
      const response = await request(url, options, timeout);
×
NEW
126
      circuitBreakerService.recordSuccess(url);
×
127

NEW
128
      return response;
×
129
    } catch (error) {
NEW
130
      if (
×
131
        (error instanceof NetworkResponseError &&
×
132
          error.response.status >= 500) ||
133
        error instanceof CircuitBreakerException
134
      ) {
NEW
135
        const circuit = circuitBreakerService.getOrRegisterCircuit(url);
×
NEW
136
        circuitBreakerService.recordFailure(circuit);
×
137
      }
138

NEW
139
      throw error;
×
140
    }
141
  };
142
}
143

144
function createCachedRequestFunction(
145
  request: <T>(
146
    url: string,
147
    options: RequestInit,
148
    timeout?: number,
149
    useCircuitBreaker?: boolean,
150
  ) => Promise<NetworkResponse<T>>,
151
  loggingService: ILoggingService,
152
) {
153
  return async <T>(
2✔
154
    url: string,
155
    options: RequestInit,
156
    timeout?: number,
157
    useCircuitBreaker?: boolean,
158
  ): Promise<NetworkResponse<T>> => {
159
    const key = getCacheKey(url, options, timeout);
28✔
160
    if (key in cache) {
28✔
161
      loggingService.debug({
6✔
162
        type: LogType.ExternalRequestCacheHit,
163
        url,
164
        key,
165
      });
166
    } else {
167
      loggingService.debug({
22✔
168
        type: LogType.ExternalRequestCacheMiss,
169
        url,
170
        key,
171
      });
172

173
      cache[key] = request(url, options, timeout, useCircuitBreaker)
22✔
174
        .catch((err) => {
175
          loggingService.debug({
12✔
176
            type: LogType.ExternalRequestCacheError,
177
            url,
178
            key,
179
          });
180
          throw err;
12✔
181
        })
182
        .finally(() => {
183
          delete cache[key];
22✔
184
        });
185
    }
186

187
    return cache[key];
28✔
188
  };
189
}
190

191
function getCacheKey(
192
  url: string,
193
  requestInit?: RequestInit,
194
  timeout?: number,
195
  useCircuitBreaker?: boolean,
196
): string {
197
  if (
28!
198
    !requestInit &&
14!
199
    timeout === undefined &&
200
    useCircuitBreaker === undefined
201
  ) {
UNCOV
202
    return url;
×
203
  }
204

205
  // JSON.stringify does not produce a stable key but initially
206
  // use a naive implementation for testing the implementation
207
  // TODO: Revisit this and use a more stable key
208
  const key = JSON.stringify({
28✔
209
    url,
210
    ...requestInit,
211
    timeout,
212
    useCircuitBreaker,
213
  });
214
  return hashSha1(key);
28✔
215
}
216

217
/**
218
 * A {@link Global} Module which provides HTTP support via {@link NetworkService}
219
 * Feature Modules don't need to import this module directly in order to inject
220
 * the {@link NetworkService}.
221
 *
222
 * This module should be included in the "root" application module
223
 */
224
@Global()
225
@Module({
226
  providers: [
227
    {
228
      provide: 'FetchClient',
229
      useFactory: fetchClientFactory,
230
      inject: [IConfigurationService, CircuitBreakerService, LoggingService],
231
    },
232
    { provide: NetworkService, useClass: FetchNetworkService },
233
  ],
234
  exports: [NetworkService],
235
})
236
export class NetworkModule {}
94✔
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