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-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

View File

@ -40,14 +40,24 @@ async function buildHeaders(
}
async function handleResponse<T>(response: Response): Promise<T> {
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<T>;
if (!rawBody) return {} as T;
return JSON.parse(rawBody) as T;
}
export async function apiGet<T>(
@ -98,7 +108,7 @@ export async function apiPatch<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}`, {
method: "DELETE",