🇬🇧 English | 🇹🇷 Türkçe
A professional, robust, and highly configurable HTTP client adapter designed for enterprise-grade Node.js applications. It provides a fluent API, built-in resilience patterns, and a powerful interceptor system, all natively sitting on top of the Node.js Native Fetch API, making it a zero-dependency library while allowing injection of custom HTTP clients.
- Fluent Request Builder: Construct complex HTTP requests with an intuitive, chainable API.
- Structured Exception Hierarchy: Every HTTP status code and network failure maps to a dedicated, named exception class with rich metadata,
isRetryable()signals, and structuredtoJSON()serialization. - Response Validation: Attach one or more
ResponseValidatorimplementations to any request to enforce schema constraints or business rules automatically before the response reaches your code. - Interceptor Architecture: Easily implement middleware for logging, authentication, error handling, and data transformation.
- Resilience & Reliability: Built-in support for retry policies (Exponential Backoff, etc.) and a generic Circuit Breaker to handle transient failures gracefully and prevent cascading failures in S2S communication.
- Type Safety: Fully typed requests and responses using generics, ensuring type safety across your application.
- Testable: Designed with dependency injection in mind, making it easy to mock and test.
- Interceptor Pipeline: A structured middleware chain for modifying requests, responses, and errors at a global level.
npm install @yildizpay/http-adapter
# or
yarn add @yildizpay/http-adapter
# or
pnpm add @yildizpay/http-adapterUse the RequestBuilder to create requests cleanly and concisely.
import { RequestBuilder, HttpMethod } from '@yildizpay/http-adapter';
const request = new RequestBuilder('https://api.example.com')
.setEndpoint('/users')
.setMethod(HttpMethod.POST)
.addHeader('Authorization', 'Bearer token')
.setBody({ name: 'John Doe', email: 'john@example.com' })
.build();Instantiate the HttpAdapter using the fluent builder API.
import { HttpAdapter, RetryPolicies, CircuitBreaker } from '@yildizpay/http-adapter';
const adapter = HttpAdapter.builder()
.withInterceptor(new AuthInterceptor(), new LoggingInterceptor())
.withRetryPolicy(RetryPolicies.exponential(3))
.withCircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 60000 })
.withCorrelationId() // propagate x-correlation-id header (opt-in)
.build();You can also use HttpAdapter.create() directly if you prefer a single-call approach.
const adapter = HttpAdapter.create(
[new AuthInterceptor()],
RetryPolicies.exponential(3),
undefined, // Optional custom HTTP client
new CircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 60000 }),
);Execute the request and receive a strongly-typed response.
interface UserResponse {
id: string;
name: string;
}
try {
const response = await adapter.send<UserResponse>(request);
console.log('User created:', response.data);
} catch (error) {
console.error('Request failed:', error);
}@yildizpay/http-adapter converts every raw error — HTTP failures, network-level OS errors, or totally unexpected exceptions — into a structured, typed exception class. This means your catch blocks never need to inspect raw status codes or error codes manually.
BaseAdapterException
├── HttpException (any HTTP response error)
│ ├── BadRequestException (400)
│ ├── UnauthorizedException (401)
│ ├── ForbiddenException (403)
│ ├── NotFoundException (404)
│ ├── ConflictException (409)
│ ├── UnprocessableEntityException (422)
│ ├── TooManyRequestsException (429) ← isRetryable() = true
│ ├── InternalServerErrorException (500)
│ ├── BadGatewayException (502) ← isRetryable() = true
│ ├── ServiceUnavailableException (503) ← isRetryable() = true
│ ├── GatewayTimeoutException (504) ← isRetryable() = true
│ └── ... (all 4xx / 5xx codes)
├── NetworkException (OS-level connectivity failures)
│ ├── ConnectionRefusedException (ECONNREFUSED) ← isRetryable() = true
│ ├── TimeoutException (ETIMEDOUT / ECONNABORTED / AbortError) ← isRetryable() = true
│ ├── SocketResetException (ECONNRESET) ← isRetryable() = true
│ ├── DnsResolutionException (ENOTFOUND / EAI_AGAIN)
│ └── HostUnreachableException (EHOSTUNREACH / ENETUNREACH)
├── UnknownException (any unclassifiable error)
└── CircuitBreakerOpenException (circuit is open, request not sent)
import {
NotFoundException,
TooManyRequestsException,
TimeoutException,
ConnectionRefusedException,
CircuitBreakerOpenException,
UnknownException,
} from '@yildizpay/http-adapter';
try {
const response = await adapter.send<PaymentResponse>(request);
} catch (error) {
if (error instanceof NotFoundException) {
// HTTP 404 — resource does not exist
console.error('Resource not found:', error.response.data);
} else if (error instanceof TooManyRequestsException) {
// HTTP 429 — back off before retrying
const retryAfterMs = error.getRetryAfterMs();
console.warn(`Rate limited. Retry after ${retryAfterMs}ms`);
} else if (error instanceof TimeoutException) {
// ETIMEDOUT / AbortError — downstream service too slow
console.error('Request timed out:', error.code);
} else if (error instanceof ConnectionRefusedException) {
// ECONNREFUSED — downstream service is down
console.error('Service is down:', error.requestContext?.url);
} else if (error instanceof CircuitBreakerOpenException) {
// Circuit is open — fail fast without hitting the server
console.error('Circuit breaker is open. Not sending request.');
} else if (error instanceof UnknownException) {
// Something unexpected — log and investigate
console.error('Unhandled error:', error.toJSON());
}
}If you prefer narrowing without instanceof (useful in functional pipelines or when crossing module boundaries), every exception class has a corresponding type guard:
import {
isHttpException,
isTimeoutException,
isConnectionRefusedException,
isCircuitBreakerOpenException,
} from '@yildizpay/http-adapter';
function handleError(error: unknown): void {
if (isTimeoutException(error)) {
// TypeScript now knows: error is TimeoutException
scheduleRetry(error.requestContext?.url);
} else if (isHttpException(error)) {
// TypeScript now knows: error is HttpException
reportToMonitoring(error.response.status, error.response.data);
}
}Each exception exposes an isRetryable(): boolean method that reflects whether the failure is transient and worth retrying. This is useful when implementing custom retry decorators or deciding at the application layer whether to propagate or retry an error.
} catch (error) {
if (error instanceof BaseAdapterException && error.isRetryable()) {
return retryOperation();
}
throw error;
}Retryable exceptions: TooManyRequestsException (429), BadGatewayException (502), ServiceUnavailableException (503), GatewayTimeoutException (504), TimeoutException, SocketResetException, ConnectionRefusedException.
All exceptions override toJSON(), making them compatible with structured loggers (Pino, Winston, etc.). JSON.stringify(error) produces a complete, nested log entry instead of an empty {}.
} catch (error) {
if (error instanceof BaseAdapterException) {
logger.error(error.toJSON());
// {
// name: 'NotFoundException',
// message: 'Not Found',
// code: 'ERR_NOT_FOUND',
// stack: '...',
// response: {
// status: 404,
// data: { detail: 'Payment record not found' },
// request: { method: 'GET', url: 'https://api.example.com/payments/123', correlationId: 'corr-abc' }
// }
// }
}
}Every exception automatically carries a RequestContext object (method, url, correlationId) sourced from the originating request. Headers and body are deliberately excluded to prevent accidental auth-token or PII leakage in logs.
} catch (error) {
if (error instanceof NetworkException) {
logger.warn({
event: 'network_failure',
exception: error.name,
request: error.requestContext, // { method, url, correlationId }
});
}
}Attach validators to a request to automatically enforce schema constraints or business rules on the response before it reaches your code. Validators run sequentially after the HTTP call succeeds and before response-side interceptors. The first validator that throws halts the chain.
import { ResponseValidator, ValidationException, Response } from '@yildizpay/http-adapter';
class PaymentStatusValidator implements ResponseValidator<IyzicoResponse> {
validate(response: Response<IyzicoResponse>): void {
if (response.data.status !== 'success') {
throw new ValidationException(
`Payment failed: ${response.data.errorMessage}`,
response,
);
}
}
}
// Works with any schema validation library — zero coupling to Zod, Joi, etc.
class PaymentSchemaValidator implements ResponseValidator<unknown> {
validate(response: Response<unknown>): void {
IyzicoResponseSchema.parse(response.data); // Zod throws on mismatch
}
}
const request = new RequestBuilder('https://api.iyzipay.com')
.setEndpoint('/payment/auth')
.setMethod(HttpMethod.POST)
.setBody(dto)
.validateWith(new PaymentSchemaValidator(), new PaymentStatusValidator())
.build();Catching a validation failure:
import { isValidationException } from '@yildizpay/http-adapter';
} catch (error) {
if (isValidationException(error)) {
console.error('Validation failed:', error.message);
console.error('Raw response:', error.response.data);
}
}Non-BaseAdapterException errors thrown inside a validator (e.g. ZodError) are automatically wrapped in ValidationException with the original error available as cause. Use the generic parameter for typed access:
} catch (error) {
if (isValidationException<ZodError>(error) && error.cause) {
console.error('Schema issues:', error.cause.issues);
}
}The full interceptor lifecycle when validators are registered:
onRequest → HTTP call → onResponse → validators → onResponseValidated → caller
↓ (on failure)
onError
onResponse always fires. onResponseValidated only fires when all validators pass — ideal for caching or downstream side effects that require a business-valid response.
You can also catch and transform exceptions at the interceptor layer before they reach your business logic.
import {
HttpErrorInterceptor,
Request,
BaseAdapterException,
UnauthorizedException,
} from '@yildizpay/http-adapter';
export class GlobalErrorInterceptor implements HttpErrorInterceptor {
async onError(error: BaseAdapterException, request: Request): Promise<never> {
if (error instanceof UnauthorizedException) {
await this.tokenService.refresh();
}
// Re-throw so the caller can handle it
throw error;
}
}Network instability is inevitable. This adapter allows you to define robust retry strategies.
| Policy | Factory | Behaviour |
|---|---|---|
| Exponential Backoff | RetryPolicies.exponential(attempts) |
Delay doubles with each attempt plus small jitter — default choice |
| Fixed Delay | RetryPolicies.fixedDelay(attempts, delayMs) |
Constant wait between every retry |
| Linear Backoff | RetryPolicies.linearBackoff(attempts, stepMs) |
Delay grows linearly (attempt × stepMs) |
| Full Jitter | RetryPolicies.fullJitter(attempts, baseMs) |
Fully random delay within exponential cap — best for spreading concurrent load |
| Decorrelated Jitter | RetryPolicies.decorrelatedJitter(attempts, baseMs, maxDelayMs) |
AWS-recommended algorithm with widest spread across concurrent clients |
import { RetryPolicies } from '@yildizpay/http-adapter';
// All policies retry on 429, 502, 503, 504 and network errors by default
RetryPolicies.exponential(3);
RetryPolicies.fixedDelay(3, 1000); // 1 s between each attempt
RetryPolicies.linearBackoff(3, 500); // 500 ms, 1000 ms, 1500 ms
RetryPolicies.fullJitter(3, 100); // random within [0, 2^attempt * 100 ms]
RetryPolicies.decorrelatedJitter(3, 100); // AWS decorrelated jitter, cap 30 sOverride the default retry decision (error.isRetryable()) for any policy via retryIf(). Accepts a plain function or a class implementing RetryPredicate.
import { RetryPolicies, RetryPredicate, BaseAdapterException, isNetworkException } from '@yildizpay/http-adapter';
// Inline function
const policy = RetryPolicies.exponential(3)
.retryIf((error) => isNetworkException(error));
// Class-based predicate
class BusinessRetryPredicate implements RetryPredicate {
shouldRetry(error: BaseAdapterException): boolean {
return error.isRetryable() && myCircuitIsAllowing();
}
}
const policy = RetryPolicies.fullJitter(3).retryIf(new BusinessRetryPredicate());To protect your system from waiting for a completely down downstream service, you can employ the CircuitBreaker. It opens the circuit after a configured amount of consecutive failures and replies instantaneously with CircuitBreakerOpenException without hitting the unresponsive server.
import { CircuitBreaker, CircuitBreakerOpenException } from '@yildizpay/http-adapter';
const breaker = new CircuitBreaker({
failureThreshold: 5, // Trip after 5 failures
resetTimeoutMs: 30000, // Try a 'half-open' request after 30 seconds
successThreshold: 1, // Close circuit after 1 successful half-open request
});
// CircuitBreakerOpenException carries when the circuit will allow the next probe
try {
await adapter.send(request);
} catch (err) {
if (err instanceof CircuitBreakerOpenException) {
console.warn(`Circuit is open. Retry after ${err.retryAfterMs()}ms`);
}
} [CLOSED] ──(failureThreshold reached)──▶ [OPEN]
▲ │
│ (resetTimeoutMs)
│ │
└──(successThreshold met)──── [HALF_OPEN] ──(failure)──▶ [OPEN]
Node.js runs on a single-threaded event loop, but async/await introduces cooperative multitasking: while one coroutine is suspended at an await, the event loop is free to start other coroutines. Without a guard, every request arriving during HALF_OPEN would read the same state and proceed concurrently — potentially overwhelming a service that has only just started to recover.
To prevent this, the circuit breaker uses a probe flag: only the first caller gets the probe slot; all subsequent concurrent callers receive CircuitBreakerOpenException until the probe resolves. This is a deliberate trade-off — a few requests are rejected in exchange for a controlled, safe recovery test.
The adapter ships with a two-tier observability system. Observers are read-only — they cannot modify requests or responses. Use them for metrics, structured logging, and distributed tracing. Use interceptors when you need to mutate the pipeline.
Attach a single observer to the adapter via .withObserver() on the builder.
| Hook | When it fires |
|---|---|
onRequestStart(request) |
After all request interceptors, immediately before the HTTP call |
onRequestSuccess(response, durationMs) |
After a successful response (includes retry time if retries occurred) |
onRequestFailure(error, durationMs) |
When the final error is propagated to the caller |
onRetry(attempt, error, delayMs) |
Each time a retry is scheduled, before the backoff delay |
import { HttpAdapterObserver, HttpAdapter, RetryPolicies } from '@yildizpay/http-adapter';
class MetricsObserver implements HttpAdapterObserver {
onRequestSuccess(_response: Response, durationMs: number): void {
metrics.histogram('http.request.duration', durationMs);
}
onRequestFailure(error: BaseAdapterException, durationMs: number): void {
metrics.increment('http.request.error', { type: error.name });
}
onRetry(attempt: number, _error: BaseAdapterException, delayMs: number): void {
logger.warn(`Retry attempt ${attempt} in ${delayMs}ms`);
}
}
const adapter = HttpAdapter.builder()
.withRetryPolicy(RetryPolicies.exponential(3))
.withObserver(new MetricsObserver())
.build();Attach an observer to a CircuitBreaker instance via the fluent .observe() method.
| Hook | When it fires |
|---|---|
onStateChange(from, to) |
On every state transition (CLOSED↔OPEN↔HALF_OPEN) |
onSuccess() |
After every successful execution |
onFailure(error) |
When a failure is counted (i.e. isFailure predicate returned true) |
onProbeRejected() |
When a concurrent caller is turned away in HALF_OPEN |
import { CircuitBreaker, CircuitBreakerObserver, CircuitState } from '@yildizpay/http-adapter';
class CircuitMetricsObserver implements CircuitBreakerObserver {
onStateChange(from: CircuitState, to: CircuitState): void {
logger.warn(`Circuit breaker: ${from} → ${to}`);
metrics.increment('circuit_breaker.state_change', { from, to });
}
onProbeRejected(): void {
metrics.increment('circuit_breaker.probe_rejected');
}
}
const adapter = HttpAdapter.builder()
.withCircuitBreaker(
new CircuitBreaker({ failureThreshold: 5 })
.observe(new CircuitMetricsObserver()),
)
.withObserver(new MetricsObserver())
.build();Thanks to the Interface Segregation Principle (ISP), you aren't forced to implement massive interfaces. You can hook into the exact lifecycle event you need by implementing HttpRequestInterceptor, HttpResponseInterceptor, or HttpErrorInterceptor.
Add common headers like Authorization tokens before requests leave.
import { HttpRequestInterceptor, Request } from '@yildizpay/http-adapter';
export class AuthInterceptor implements HttpRequestInterceptor {
async onRequest(request: Request): Promise<Request> {
request.addHeader('Authorization', 'Bearer my-secret-token');
return request;
}
}Inspect or mutate payloads identically across all incoming responses.
import { HttpResponseInterceptor, Response } from '@yildizpay/http-adapter';
export class TransformResponseInterceptor implements HttpResponseInterceptor {
async onResponse(response: Response): Promise<Response> {
if (response.status === 201) {
console.log('Resource successfully created!');
}
return response;
}
}Catch network failures or non-success HTTP statuses centrally.
import {
HttpErrorInterceptor,
Request,
BaseAdapterException,
UnauthorizedException,
} from '@yildizpay/http-adapter';
export class GlobalErrorInterceptor implements HttpErrorInterceptor {
async onError(error: BaseAdapterException, request: Request): Promise<BadRequestException> {
if (error instanceof UnauthorizedException) {
console.error(`Unauthorized access to ${error.requestContext?.url}! Redirecting to login...`);
}
throw error;
}
}Every request automatically generates a systemCorrelationId used internally for logging and error context. You can also forward it as an outgoing header to downstream services.
Propagation is opt-in — enable it on the adapter with .withCorrelationId():
const adapter = HttpAdapter.builder()
.withCorrelationId() // forwards as 'x-correlation-id' (default)
.withCorrelationId('x-request-id') // use a custom header name
.build();Per-request overrides take full precedence over the adapter-level config:
const request = new RequestBuilder('https://api.example.com')
.setEndpoint('/payments')
.withCorrelationId('x-trace-id') // enable for this request with a custom header
.build();
const request2 = new RequestBuilder('https://api.example.com')
.setEndpoint('/internal')
.withoutCorrelationId() // disable for this request (adapter config ignored)
.build();Header resolution order: per-request header → adapter header → 'x-correlation-id'.
The adapter's global configuration (retry policy, circuit breaker, interceptors) applies to every request by default. For cases where a single request needs different behaviour, RequestBuilder exposes per-request overrides that take full precedence over the global config.
import { RetryPolicies } from '@yildizpay/http-adapter';
// Override: use a different policy for this request
const request = new RequestBuilder('https://api.example.com')
.setEndpoint('/v1/payments')
.withRetryPolicy(RetryPolicies.decorrelatedJitter(5))
.build();
// Disable: no retries for this request, regardless of global policy
const sensitiveRequest = new RequestBuilder('https://api.example.com')
.setEndpoint('/v1/refunds')
.withoutRetry()
.build();// Override: use a dedicated circuit breaker for this request
const request = new RequestBuilder('https://api.example.com')
.setEndpoint('/v1/payments')
.withCircuitBreaker(new CircuitBreaker({ failureThreshold: 3 }))
.build();
// Bypass: skip the circuit breaker entirely for this request
const probeRequest = new RequestBuilder('https://api.example.com')
.setEndpoint('/health')
.withoutCircuitBreaker()
.build();// Exclude all instances of a class (e.g. skip logging for sensitive requests)
const request = new RequestBuilder('https://api.example.com')
.setEndpoint('/v1/payments')
.withoutInterceptor(LoggingInterceptor)
.build();
// Exclude a specific instance (when multiple instances of the same class are registered)
const loggingInterceptor = new LoggingInterceptor('payments');
const request = new RequestBuilder('https://api.example.com')
.setEndpoint('/v1/payments')
.withoutInterceptorInstance(loggingInterceptor)
.build();@yildizpay/http-adapter ships a dedicated testing sub-path that provides purpose-built test doubles, spies, and no-op helpers. Import them without polluting your production bundle:
import {
MockHttpAdapter,
MockHttpClient,
NoopInterceptor,
SpyInterceptor,
SpyObserver,
} from '@yildizpay/http-adapter/testing';A full in-memory test double for HttpAdapter that implements HttpAdapterContract. Use it in unit tests to control responses without making real HTTP calls.
const adapter = new MockHttpAdapter();
// Always return this response
adapter.mockResolvedValue({ STATUS: 'SUCCESS', ORDER_ID: '123' });
// Always throw this error
adapter.mockRejectedValue(new ServiceUnavailableException(...));
// One-time responses consumed in FIFO order, falling back to the default
adapter
.mockResolvedOnce({ STATUS: 'PENDING' })
.mockResolvedOnce({ STATUS: 'SUCCESS' })
.mockResolvedValue({ STATUS: 'UNKNOWN' }); // fallback
// Custom factory — receives the full Request object
adapter.mockImplementation((request) => ({
STATUS: request.body?.type === 'REFUND' ? 'REFUNDED' : 'SUCCESS',
}));Use onEndpoint() to scope responses and assertions to a single path. Endpoint responses take priority over global ones.
adapter.onEndpoint('/api/payments').mockResolvedValue({ STATUS: 'SUCCESS' });
adapter.onEndpoint('/api/refunds').mockRejectedValue(new NotFoundException(...));
// One-time queue per endpoint
adapter.onEndpoint('/api/payments')
.mockResolvedOnce({ STATUS: 'PENDING' })
.mockResolvedValue({ STATUS: 'SUCCESS' });// After running the code under test:
adapter.assertCalledTimes(2);
adapter.assertCalledWith('/api/payments', { method: HttpMethod.POST });
adapter.assertCalledWithBody(0, { AMOUNT: '100', CURRENCY: 'TRY' });
adapter.assertNthCalledWith(1, '/api/payments');
adapter.assertLastCalledWith('/api/refunds');
adapter.assertCallOrder('/api/payments', '/api/refunds');
adapter.assertNotCalled();
// Convenience getters
adapter.callCount; // number
adapter.firstCall; // Request | undefined
adapter.lastCall; // Request | undefined
adapter.wasCalled(); // boolean
adapter.wasNotCalled(); // booleanEndpoint scopes expose the same assertion API, scoped to their path:
const scope = adapter.onEndpoint('/api/payments');
scope.assertCalledTimes(1);
scope.assertCalledWith({ body: { MERCHANT_ID: 'M001' } });
scope.wasCalled();All assertCalledWith variants accept an optional RequestMatcher for deep partial matching on method, body, headers, and queryParams. Only the fields you specify are checked; extra fields in the actual request are ignored.
adapter.assertCalledWith('/api/payments', {
method: HttpMethod.POST,
body: { AMOUNT: '100' }, // extra keys in actual body are ignored
headers: { 'x-merchant-id': 'M001' },
});Body matching uses deep partial equality — nested objects are matched partially, NaN is handled correctly via Object.is, Date instances are compared by value, and arrays are never confused with plain objects.
Enable strict mode to fail fast whenever a call is made to an unregistered endpoint, even when a global default is configured. Useful for catching unexpected HTTP calls in tests.
const adapter = new MockHttpAdapter({ strict: true });
adapter.onEndpoint('/api/payments').mockResolvedValue({ STATUS: 'SUCCESS' });
// This throws immediately — '/api/users' is not registered
await adapter.send(request);reset() clears all calls, queues, and default behaviors without invalidating existing onEndpoint() references.
beforeEach(() => adapter.reset());A lower-level test double for the HttpClientContract transport layer. Use it when testing custom HttpClient wrappers rather than full adapter pipelines. Exposes the same queue API and assertion helpers as MockHttpAdapter.
const client = new MockHttpClient();
client.mockResolvedValue({ data: { id: 1 }, status: 200, headers: {} });
const result = await client.request(config);
client.assertCalledTimes(1);
client.assertCalledWith({ method: HttpMethod.POST });Pass-through implementations that satisfy a contract without any side effects. Useful when a hook must be provided but its behaviour is irrelevant to the test under execution.
| Class | Implements |
|---|---|
NoopInterceptor |
All four HttpInterceptor hooks — returns each value unchanged |
NoopObserver |
All HttpAdapterObserver hooks — empty methods |
NoopCircuitBreakerObserver |
All CircuitBreakerObserver hooks — empty methods |
const adapter = HttpAdapter.builder()
.withInterceptor(new NoopInterceptor())
.withObserver(new NoopObserver())
.build();Record every invocation and pass values through unchanged. Use them when you need to assert that a hook was called without mocking the full request pipeline.
const interceptorSpy = new SpyInterceptor();
const observerSpy = new SpyObserver();
const adapter = HttpAdapter.builder()
.withInterceptor(interceptorSpy)
.withObserver(observerSpy)
.build();
await adapter.send(request);
// SpyInterceptor
expect(interceptorSpy.requestCalls).toHaveLength(1);
expect(interceptorSpy.responseCalls).toHaveLength(1);
expect(interceptorSpy.errorCalls).toHaveLength(0);
// SpyObserver
expect(observerSpy.requestStartCalls).toHaveLength(1);
expect(observerSpy.successCalls[0].durationMs).toBeGreaterThan(0);
// Reset between tests
interceptorSpy.reset();
observerSpy.reset();| Spy | Recorded arrays |
|---|---|
SpyInterceptor |
requestCalls, responseCalls, responseValidatedCalls, errorCalls |
SpyObserver |
requestStartCalls, successCalls, failureCalls, retryCalls |
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License.
