From 02e8c2a89bc7136fcfd78b609b4ba786d74e3709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Murat=20=C3=96ZDEM=C4=B0R?= Date: Tue, 24 Mar 2026 19:08:22 +0300 Subject: [PATCH] Improves API client response handling and idempotency Extends `X-Idempotency-Key` header usage to `DELETE` and `PUT` requests, ensuring idempotent behavior for a wider range of modifying API operations. Updates the documentation and client implementation for `DELETE` requests. Refactors `handleResponse` to robustly parse API responses. It now reads the response body once as text, enabling graceful handling of non-JSON error bodies and consistent processing of successful or empty responses. --- README.md | 4 ++-- src/client.ts | 22 ++++++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index aa9c606..cf4c1fd 100644 --- a/README.md +++ b/README.md @@ -610,7 +610,7 @@ Her API isteğine (`login` ve `refresh` dahil) aşağıdaki header'lar eklenir: | `X-Signature` | hex string | HMAC-SHA256 imzası (bkz. aşağısı) | | `X-Timestamp` | `Date.now()` string | Unix epoch, milisaniye | | `X-Nonce` | UUID v4 | Her istekte tekil, replay saldırısı önleme | -| `X-Idempotency-Key` | UUID v4 | Yalnızca `POST` ve `PATCH` isteklerinde | +| `X-Idempotency-Key` | UUID v4 | `POST`, `PUT`, `PATCH` ve `DELETE` isteklerinde | ### HMAC-SHA256 İmza Hesabı @@ -664,7 +664,7 @@ X-Signature = HMAC-SHA256(imzalanacak, secret) → "7be41d..." | `X-Signature` | ✅ | ✅ | ✅ | | `X-Timestamp` | ✅ | ✅ | ✅ | | `X-Nonce` | ✅ | ✅ | ✅ | -| `X-Idempotency-Key` | ✅ | ✅ | ✅ (POST/PATCH) | +| `X-Idempotency-Key` | ✅ | ✅ | ✅ (POST/PUT/PATCH/DELETE) | ### Güvenlik Önerileri diff --git a/src/client.ts b/src/client.ts index 42d600f..ab68f38 100644 --- a/src/client.ts +++ b/src/client.ts @@ -40,14 +40,24 @@ async function buildHeaders( } async function handleResponse(response: Response): Promise { + const rawBody = await response.text(); + if (!response.ok) { - const error = await response.json().catch(() => ({})); - const msg = (error as { message?: string }).message ?? response.statusText; + let msg = response.statusText; + if (rawBody) { + try { + const error = JSON.parse(rawBody) as { message?: string }; + msg = error.message ?? rawBody; + } catch { + msg = rawBody; + } + } throw new Error(`API error [${response.status}]: ${msg}`); } - // 204 No Content - if (response.status === 204) return {} as T; - return response.json() as Promise; + + if (!rawBody) return {} as T; + + return JSON.parse(rawBody) as T; } export async function apiGet( @@ -98,7 +108,7 @@ export async function apiPatch( } export async function apiDelete(path: string): Promise { - const headers = await buildHeaders("DELETE", path, "", false); + const headers = await buildHeaders("DELETE", path, "", true); const response = await fetch(`${config.baseUrl}${path}`, { method: "DELETE",