@yildizpay/http-adapter 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -9
- package/README.tr.md +288 -44
- package/dist/builders/request.builder.d.ts +3 -0
- package/dist/builders/request.builder.js +6 -1
- package/dist/builders/request.builder.js.map +1 -1
- package/dist/contracts/http-client.contract.d.ts +0 -9
- package/dist/contracts/http-client.contract.js +0 -17
- package/dist/contracts/http-client.contract.js.map +1 -1
- package/dist/contracts/http-interceptor.contract.d.ts +6 -2
- package/dist/contracts/response-validator.contract.d.ts +4 -0
- package/dist/contracts/response-validator.contract.js +3 -0
- package/dist/contracts/response-validator.contract.js.map +1 -0
- package/dist/core/default-http-client.js +7 -6
- package/dist/core/default-http-client.js.map +1 -1
- package/dist/core/error.converter.d.ts +5 -0
- package/dist/core/error.converter.js +31 -0
- package/dist/core/error.converter.js.map +1 -0
- package/dist/core/http.adapter.js +28 -4
- package/dist/core/http.adapter.js.map +1 -1
- package/dist/exceptions/base-adapter.exception.d.ts +7 -0
- package/dist/exceptions/base-adapter.exception.js +34 -0
- package/dist/exceptions/base-adapter.exception.js.map +1 -0
- package/dist/exceptions/circuit-breaker-open.exception.d.ts +2 -2
- package/dist/exceptions/circuit-breaker-open.exception.js +3 -3
- package/dist/exceptions/circuit-breaker-open.exception.js.map +1 -1
- package/dist/exceptions/exception.guards.d.ts +17 -0
- package/dist/exceptions/exception.guards.js +53 -0
- package/dist/exceptions/exception.guards.js.map +1 -0
- package/dist/exceptions/http-exception.factory.d.ts +5 -0
- package/dist/exceptions/http-exception.factory.js +59 -0
- package/dist/exceptions/http-exception.factory.js.map +1 -0
- package/dist/exceptions/http-status.exceptions.d.ts +134 -0
- package/dist/exceptions/http-status.exceptions.js +382 -0
- package/dist/exceptions/http-status.exceptions.js.map +1 -0
- package/dist/exceptions/network-exception.factory.d.ts +6 -0
- package/dist/exceptions/network-exception.factory.js +35 -0
- package/dist/exceptions/network-exception.factory.js.map +1 -0
- package/dist/exceptions/network.exceptions.d.ts +25 -0
- package/dist/exceptions/network.exceptions.js +69 -0
- package/dist/exceptions/network.exceptions.js.map +1 -0
- package/dist/exceptions/unknown.exception.d.ts +7 -0
- package/dist/exceptions/unknown.exception.js +20 -0
- package/dist/exceptions/unknown.exception.js.map +1 -0
- package/dist/exceptions/validation.exception.d.ts +9 -0
- package/dist/exceptions/validation.exception.js +25 -0
- package/dist/exceptions/validation.exception.js.map +1 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/models/request-context.d.ts +5 -0
- package/dist/models/request-context.js +3 -0
- package/dist/models/request-context.js.map +1 -0
- package/dist/models/request.d.ts +3 -1
- package/dist/models/request.js +2 -1
- package/dist/models/request.js.map +1 -1
- package/dist/models/response.d.ts +8 -5
- package/dist/models/response.js +9 -5
- package/dist/models/response.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -3
- package/dist/exceptions/http.exception.d.ts +0 -5
- package/dist/exceptions/http.exception.js +0 -12
- package/dist/exceptions/http.exception.js.map +0 -1
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ A professional, robust, and highly configurable HTTP client adapter designed for
|
|
|
11
11
|
## Key Features
|
|
12
12
|
|
|
13
13
|
- **Fluent Request Builder:** Construct complex HTTP requests with an intuitive, chainable API.
|
|
14
|
+
- **Structured Exception Hierarchy:** Every HTTP status code and network failure maps to a dedicated, named exception class with rich metadata, `isRetryable()` signals, and structured `toJSON()` serialization.
|
|
15
|
+
- **Response Validation:** Attach one or more `ResponseValidator` implementations to any request to enforce schema constraints or business rules automatically before the response reaches your code.
|
|
14
16
|
- **Interceptor Architecture:** Easily implement middleware for logging, authentication, error handling, and data transformation.
|
|
15
17
|
- **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.
|
|
16
18
|
- **Type Safety:** Fully typed requests and responses using generics, ensuring type safety across your application.
|
|
@@ -53,7 +55,7 @@ import { HttpAdapter, RetryPolicies, CircuitBreaker } from '@yildizpay/http-adap
|
|
|
53
55
|
|
|
54
56
|
const circuitBreaker = new CircuitBreaker({
|
|
55
57
|
failureThreshold: 5,
|
|
56
|
-
resetTimeoutMs: 60000,
|
|
58
|
+
resetTimeoutMs: 60000,
|
|
57
59
|
});
|
|
58
60
|
|
|
59
61
|
const adapter = HttpAdapter.create(
|
|
@@ -62,7 +64,7 @@ const adapter = HttpAdapter.create(
|
|
|
62
64
|
],
|
|
63
65
|
RetryPolicies.exponential(3), // Retry up to 3 times with exponential backoff
|
|
64
66
|
undefined, // Optional custom HTTP client
|
|
65
|
-
circuitBreaker
|
|
67
|
+
circuitBreaker, // Optional Circuit Breaker
|
|
66
68
|
);
|
|
67
69
|
```
|
|
68
70
|
|
|
@@ -84,6 +86,241 @@ try {
|
|
|
84
86
|
}
|
|
85
87
|
```
|
|
86
88
|
|
|
89
|
+
## Error Handling
|
|
90
|
+
|
|
91
|
+
`@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.
|
|
92
|
+
|
|
93
|
+
### Exception Hierarchy
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
BaseAdapterException
|
|
97
|
+
├── HttpException (any HTTP response error)
|
|
98
|
+
│ ├── BadRequestException (400)
|
|
99
|
+
│ ├── UnauthorizedException (401)
|
|
100
|
+
│ ├── ForbiddenException (403)
|
|
101
|
+
│ ├── NotFoundException (404)
|
|
102
|
+
│ ├── ConflictException (409)
|
|
103
|
+
│ ├── UnprocessableEntityException (422)
|
|
104
|
+
│ ├── TooManyRequestsException (429) ← isRetryable() = true
|
|
105
|
+
│ ├── InternalServerErrorException (500)
|
|
106
|
+
│ ├── BadGatewayException (502) ← isRetryable() = true
|
|
107
|
+
│ ├── ServiceUnavailableException (503) ← isRetryable() = true
|
|
108
|
+
│ ├── GatewayTimeoutException (504) ← isRetryable() = true
|
|
109
|
+
│ └── ... (all 4xx / 5xx codes)
|
|
110
|
+
├── NetworkException (OS-level connectivity failures)
|
|
111
|
+
│ ├── ConnectionRefusedException (ECONNREFUSED) ← isRetryable() = true
|
|
112
|
+
│ ├── TimeoutException (ETIMEDOUT / ECONNABORTED / AbortError) ← isRetryable() = true
|
|
113
|
+
│ ├── SocketResetException (ECONNRESET) ← isRetryable() = true
|
|
114
|
+
│ ├── DnsResolutionException (ENOTFOUND / EAI_AGAIN)
|
|
115
|
+
│ └── HostUnreachableException (EHOSTUNREACH / ENETUNREACH)
|
|
116
|
+
├── UnknownException (any unclassifiable error)
|
|
117
|
+
└── CircuitBreakerOpenException (circuit is open, request not sent)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Catching Exceptions by Type
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import {
|
|
124
|
+
NotFoundException,
|
|
125
|
+
TooManyRequestsException,
|
|
126
|
+
TimeoutException,
|
|
127
|
+
ConnectionRefusedException,
|
|
128
|
+
CircuitBreakerOpenException,
|
|
129
|
+
UnknownException,
|
|
130
|
+
} from '@yildizpay/http-adapter';
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const response = await adapter.send<PaymentResponse>(request);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof NotFoundException) {
|
|
136
|
+
// HTTP 404 — resource does not exist
|
|
137
|
+
console.error('Resource not found:', error.response.data);
|
|
138
|
+
} else if (error instanceof TooManyRequestsException) {
|
|
139
|
+
// HTTP 429 — back off before retrying
|
|
140
|
+
const retryAfterMs = error.getRetryAfterMs();
|
|
141
|
+
console.warn(`Rate limited. Retry after ${retryAfterMs}ms`);
|
|
142
|
+
} else if (error instanceof TimeoutException) {
|
|
143
|
+
// ETIMEDOUT / AbortError — downstream service too slow
|
|
144
|
+
console.error('Request timed out:', error.code);
|
|
145
|
+
} else if (error instanceof ConnectionRefusedException) {
|
|
146
|
+
// ECONNREFUSED — downstream service is down
|
|
147
|
+
console.error('Service is down:', error.requestContext?.url);
|
|
148
|
+
} else if (error instanceof CircuitBreakerOpenException) {
|
|
149
|
+
// Circuit is open — fail fast without hitting the server
|
|
150
|
+
console.error('Circuit breaker is open. Not sending request.');
|
|
151
|
+
} else if (error instanceof UnknownException) {
|
|
152
|
+
// Something unexpected — log and investigate
|
|
153
|
+
console.error('Unhandled error:', error.toJSON());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Type Guards
|
|
159
|
+
|
|
160
|
+
If you prefer narrowing without `instanceof` (useful in functional pipelines or when crossing module boundaries), every exception class has a corresponding type guard:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import {
|
|
164
|
+
isHttpException,
|
|
165
|
+
isTimeoutException,
|
|
166
|
+
isConnectionRefusedException,
|
|
167
|
+
isCircuitBreakerOpenException,
|
|
168
|
+
} from '@yildizpay/http-adapter';
|
|
169
|
+
|
|
170
|
+
function handleError(error: unknown): void {
|
|
171
|
+
if (isTimeoutException(error)) {
|
|
172
|
+
// TypeScript now knows: error is TimeoutException
|
|
173
|
+
scheduleRetry(error.requestContext?.url);
|
|
174
|
+
} else if (isHttpException(error)) {
|
|
175
|
+
// TypeScript now knows: error is HttpException
|
|
176
|
+
reportToMonitoring(error.response.status, error.response.data);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### `isRetryable()` Signal
|
|
182
|
+
|
|
183
|
+
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.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error instanceof BaseAdapterException && error.isRetryable()) {
|
|
188
|
+
return retryOperation();
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Retryable exceptions: `TooManyRequestsException (429)`, `BadGatewayException (502)`, `ServiceUnavailableException (503)`, `GatewayTimeoutException (504)`, `TimeoutException`, `SocketResetException`, `ConnectionRefusedException`.
|
|
195
|
+
|
|
196
|
+
### Structured Logging with `toJSON()`
|
|
197
|
+
|
|
198
|
+
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 `{}`.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error instanceof BaseAdapterException) {
|
|
203
|
+
logger.error(error.toJSON());
|
|
204
|
+
// {
|
|
205
|
+
// name: 'NotFoundException',
|
|
206
|
+
// message: 'Not Found',
|
|
207
|
+
// code: 'ERR_NOT_FOUND',
|
|
208
|
+
// stack: '...',
|
|
209
|
+
// response: {
|
|
210
|
+
// status: 404,
|
|
211
|
+
// data: { detail: 'Payment record not found' },
|
|
212
|
+
// request: { method: 'GET', url: 'https://api.example.com/payments/123', correlationId: 'corr-abc' }
|
|
213
|
+
// }
|
|
214
|
+
// }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `RequestContext` — Safe Request Metadata
|
|
220
|
+
|
|
221
|
+
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.
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error instanceof NetworkException) {
|
|
226
|
+
logger.warn({
|
|
227
|
+
event: 'network_failure',
|
|
228
|
+
exception: error.name,
|
|
229
|
+
request: error.requestContext, // { method, url, correlationId }
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Response Validators
|
|
236
|
+
|
|
237
|
+
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.
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import { ResponseValidator, ValidationException, Response } from '@yildizpay/http-adapter';
|
|
241
|
+
|
|
242
|
+
class PaymentStatusValidator implements ResponseValidator<IyzicoResponse> {
|
|
243
|
+
validate(response: Response<IyzicoResponse>): void {
|
|
244
|
+
if (response.data.status !== 'success') {
|
|
245
|
+
throw new ValidationException(
|
|
246
|
+
`Payment failed: ${response.data.errorMessage}`,
|
|
247
|
+
response,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Works with any schema validation library — zero coupling to Zod, Joi, etc.
|
|
254
|
+
class PaymentSchemaValidator implements ResponseValidator<unknown> {
|
|
255
|
+
validate(response: Response<unknown>): void {
|
|
256
|
+
IyzicoResponseSchema.parse(response.data); // Zod throws on mismatch
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const request = new RequestBuilder('https://api.iyzipay.com')
|
|
261
|
+
.setEndpoint('/payment/auth')
|
|
262
|
+
.setMethod(HttpMethod.POST)
|
|
263
|
+
.setBody(dto)
|
|
264
|
+
.validateWith(new PaymentSchemaValidator(), new PaymentStatusValidator())
|
|
265
|
+
.build();
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Catching a validation failure:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { isValidationException } from '@yildizpay/http-adapter';
|
|
272
|
+
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (isValidationException(error)) {
|
|
275
|
+
console.error('Validation failed:', error.message);
|
|
276
|
+
console.error('Raw response:', error.response.data);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
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:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (isValidationException<ZodError>(error) && error.cause) {
|
|
286
|
+
console.error('Schema issues:', error.cause.issues);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The full interceptor lifecycle when validators are registered:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
onRequest → HTTP call → onResponse → validators → onResponseValidated → caller
|
|
295
|
+
↓ (on failure)
|
|
296
|
+
onError
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`onResponse` always fires. `onResponseValidated` only fires when all validators pass — ideal for caching or downstream side effects that require a business-valid response.
|
|
300
|
+
|
|
301
|
+
### Error Interceptor
|
|
302
|
+
|
|
303
|
+
You can also catch and transform exceptions at the interceptor layer before they reach your business logic.
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
import {
|
|
307
|
+
HttpErrorInterceptor,
|
|
308
|
+
Request,
|
|
309
|
+
BaseAdapterException,
|
|
310
|
+
UnauthorizedException,
|
|
311
|
+
} from '@yildizpay/http-adapter';
|
|
312
|
+
|
|
313
|
+
export class GlobalErrorInterceptor implements HttpErrorInterceptor {
|
|
314
|
+
async onError(error: BaseAdapterException, request: Request): Promise<never> {
|
|
315
|
+
if (error instanceof UnauthorizedException) {
|
|
316
|
+
await this.tokenService.refresh();
|
|
317
|
+
}
|
|
318
|
+
// Re-throw so the caller can handle it
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
87
324
|
## Resilience & Retries
|
|
88
325
|
|
|
89
326
|
Network instability is inevitable. This adapter allows you to define robust retry strategies.
|
|
@@ -95,7 +332,7 @@ The built-in `ExponentialBackoffPolicy` waits increasingly longer between retrie
|
|
|
95
332
|
```typescript
|
|
96
333
|
import { RetryPolicies } from '@yildizpay/http-adapter';
|
|
97
334
|
|
|
98
|
-
// Retries on 429,
|
|
335
|
+
// Retries on 429, 502, 503, 504 and network errors
|
|
99
336
|
const retryPolicy = RetryPolicies.exponential(5);
|
|
100
337
|
```
|
|
101
338
|
|
|
@@ -118,6 +355,7 @@ const breaker = new CircuitBreaker({
|
|
|
118
355
|
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`.
|
|
119
356
|
|
|
120
357
|
### 1. Request Interceptor (e.g., Auth Tokens)
|
|
358
|
+
|
|
121
359
|
Add common headers like Authorization tokens before requests leave.
|
|
122
360
|
|
|
123
361
|
```typescript
|
|
@@ -132,6 +370,7 @@ export class AuthInterceptor implements HttpRequestInterceptor {
|
|
|
132
370
|
```
|
|
133
371
|
|
|
134
372
|
### 2. Response Interceptor (e.g., Data Transformation)
|
|
373
|
+
|
|
135
374
|
Inspect or mutate payloads identically across all incoming responses.
|
|
136
375
|
|
|
137
376
|
```typescript
|
|
@@ -140,7 +379,7 @@ import { HttpResponseInterceptor, Response } from '@yildizpay/http-adapter';
|
|
|
140
379
|
export class TransformResponseInterceptor implements HttpResponseInterceptor {
|
|
141
380
|
async onResponse(response: Response): Promise<Response> {
|
|
142
381
|
if (response.status === 201) {
|
|
143
|
-
|
|
382
|
+
console.log('Resource successfully created!');
|
|
144
383
|
}
|
|
145
384
|
return response;
|
|
146
385
|
}
|
|
@@ -148,17 +387,22 @@ export class TransformResponseInterceptor implements HttpResponseInterceptor {
|
|
|
148
387
|
```
|
|
149
388
|
|
|
150
389
|
### 3. Error Interceptor (e.g., Global Error Handling)
|
|
390
|
+
|
|
151
391
|
Catch network failures or non-success HTTP statuses centrally.
|
|
152
392
|
|
|
153
393
|
```typescript
|
|
154
|
-
import {
|
|
394
|
+
import {
|
|
395
|
+
HttpErrorInterceptor,
|
|
396
|
+
Request,
|
|
397
|
+
BaseAdapterException,
|
|
398
|
+
UnauthorizedException,
|
|
399
|
+
} from '@yildizpay/http-adapter';
|
|
155
400
|
|
|
156
401
|
export class GlobalErrorInterceptor implements HttpErrorInterceptor {
|
|
157
|
-
async onError(error:
|
|
158
|
-
if (error instanceof
|
|
159
|
-
|
|
402
|
+
async onError(error: BaseAdapterException, request: Request): Promise<BadRequestException> {
|
|
403
|
+
if (error instanceof UnauthorizedException) {
|
|
404
|
+
console.error(`Unauthorized access to ${error.requestContext?.url}! Redirecting to login...`);
|
|
160
405
|
}
|
|
161
|
-
// You can throw a custom error or return a fallback payload
|
|
162
406
|
throw error;
|
|
163
407
|
}
|
|
164
408
|
}
|