@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.
Files changed (63) hide show
  1. package/README.md +253 -9
  2. package/README.tr.md +288 -44
  3. package/dist/builders/request.builder.d.ts +3 -0
  4. package/dist/builders/request.builder.js +6 -1
  5. package/dist/builders/request.builder.js.map +1 -1
  6. package/dist/contracts/http-client.contract.d.ts +0 -9
  7. package/dist/contracts/http-client.contract.js +0 -17
  8. package/dist/contracts/http-client.contract.js.map +1 -1
  9. package/dist/contracts/http-interceptor.contract.d.ts +6 -2
  10. package/dist/contracts/response-validator.contract.d.ts +4 -0
  11. package/dist/contracts/response-validator.contract.js +3 -0
  12. package/dist/contracts/response-validator.contract.js.map +1 -0
  13. package/dist/core/default-http-client.js +7 -6
  14. package/dist/core/default-http-client.js.map +1 -1
  15. package/dist/core/error.converter.d.ts +5 -0
  16. package/dist/core/error.converter.js +31 -0
  17. package/dist/core/error.converter.js.map +1 -0
  18. package/dist/core/http.adapter.js +28 -4
  19. package/dist/core/http.adapter.js.map +1 -1
  20. package/dist/exceptions/base-adapter.exception.d.ts +7 -0
  21. package/dist/exceptions/base-adapter.exception.js +34 -0
  22. package/dist/exceptions/base-adapter.exception.js.map +1 -0
  23. package/dist/exceptions/circuit-breaker-open.exception.d.ts +2 -2
  24. package/dist/exceptions/circuit-breaker-open.exception.js +3 -3
  25. package/dist/exceptions/circuit-breaker-open.exception.js.map +1 -1
  26. package/dist/exceptions/exception.guards.d.ts +17 -0
  27. package/dist/exceptions/exception.guards.js +53 -0
  28. package/dist/exceptions/exception.guards.js.map +1 -0
  29. package/dist/exceptions/http-exception.factory.d.ts +5 -0
  30. package/dist/exceptions/http-exception.factory.js +59 -0
  31. package/dist/exceptions/http-exception.factory.js.map +1 -0
  32. package/dist/exceptions/http-status.exceptions.d.ts +134 -0
  33. package/dist/exceptions/http-status.exceptions.js +382 -0
  34. package/dist/exceptions/http-status.exceptions.js.map +1 -0
  35. package/dist/exceptions/network-exception.factory.d.ts +6 -0
  36. package/dist/exceptions/network-exception.factory.js +35 -0
  37. package/dist/exceptions/network-exception.factory.js.map +1 -0
  38. package/dist/exceptions/network.exceptions.d.ts +25 -0
  39. package/dist/exceptions/network.exceptions.js +69 -0
  40. package/dist/exceptions/network.exceptions.js.map +1 -0
  41. package/dist/exceptions/unknown.exception.d.ts +7 -0
  42. package/dist/exceptions/unknown.exception.js +20 -0
  43. package/dist/exceptions/unknown.exception.js.map +1 -0
  44. package/dist/exceptions/validation.exception.d.ts +9 -0
  45. package/dist/exceptions/validation.exception.js +25 -0
  46. package/dist/exceptions/validation.exception.js.map +1 -0
  47. package/dist/index.d.ts +11 -1
  48. package/dist/index.js +11 -1
  49. package/dist/index.js.map +1 -1
  50. package/dist/models/request-context.d.ts +5 -0
  51. package/dist/models/request-context.js +3 -0
  52. package/dist/models/request-context.js.map +1 -0
  53. package/dist/models/request.d.ts +3 -1
  54. package/dist/models/request.js +2 -1
  55. package/dist/models/request.js.map +1 -1
  56. package/dist/models/response.d.ts +8 -5
  57. package/dist/models/response.js +9 -5
  58. package/dist/models/response.js.map +1 -1
  59. package/dist/tsconfig.tsbuildinfo +1 -1
  60. package/package.json +17 -3
  61. package/dist/exceptions/http.exception.d.ts +0 -5
  62. package/dist/exceptions/http.exception.js +0 -12
  63. 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 // Optional Circuit Breaker
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, 500, 502, 503, 504 and network errors
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
- console.log('Resource successfully created!');
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 { HttpErrorInterceptor, Request, HttpClientException } from '@yildizpay/http-adapter';
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: unknown, request: Request): Promise<unknown> {
158
- if (error instanceof HttpClientException && error.response?.status === 401) {
159
- console.error(`Unauthorized access to ${request.endpoint}! Redirecting to login...`);
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
  }