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.
This commit is contained in:
Murat ÖZDEMİR 2026-03-24 19:08:22 +03:00
parent 28c24d8cb1
commit 02e8c2a89b
2 changed files with 18 additions and 8 deletions

View File

@ -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-Signature` | hex string | HMAC-SHA256 imzası (bkz. aşağısı) |
| `X-Timestamp` | `Date.now()` string | Unix epoch, milisaniye | | `X-Timestamp` | `Date.now()` string | Unix epoch, milisaniye |
| `X-Nonce` | UUID v4 | Her istekte tekil, replay saldırısı önleme | | `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ı ### HMAC-SHA256 İmza Hesabı
@ -664,7 +664,7 @@ X-Signature = HMAC-SHA256(imzalanacak, secret) → "7be41d..."
| `X-Signature` | ✅ | ✅ | ✅ | | `X-Signature` | ✅ | ✅ | ✅ |
| `X-Timestamp` | ✅ | ✅ | ✅ | | `X-Timestamp` | ✅ | ✅ | ✅ |
| `X-Nonce` | ✅ | ✅ | ✅ | | `X-Nonce` | ✅ | ✅ | ✅ |
| `X-Idempotency-Key` | ✅ | ✅ | ✅ (POST/PATCH) | | `X-Idempotency-Key` | ✅ | ✅ | ✅ (POST/PUT/PATCH/DELETE) |
### Güvenlik Önerileri ### Güvenlik Önerileri

View File

@ -40,14 +40,24 @@ async function buildHeaders(
} }
async function handleResponse<T>(response: Response): Promise<T> { async function handleResponse<T>(response: Response): Promise<T> {
const rawBody = await response.text();
if (!response.ok) { if (!response.ok) {
const error = await response.json().catch(() => ({})); let msg = response.statusText;
const msg = (error as { message?: string }).message ?? 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}`); throw new Error(`API error [${response.status}]: ${msg}`);
} }
// 204 No Content
if (response.status === 204) return {} as T; if (!rawBody) return {} as T;
return response.json() as Promise<T>;
return JSON.parse(rawBody) as T;
} }
export async function apiGet<T>( export async function apiGet<T>(
@ -98,7 +108,7 @@ export async function apiPatch<T>(
} }
export async function apiDelete<T>(path: string): Promise<T> { export async function apiDelete<T>(path: string): Promise<T> {
const headers = await buildHeaders("DELETE", path, "", false); const headers = await buildHeaders("DELETE", path, "", true);
const response = await fetch(`${config.baseUrl}${path}`, { const response = await fetch(`${config.baseUrl}${path}`, {
method: "DELETE", method: "DELETE",