# VC-086 · Aynı iş yeniden gönderilince sunucu ikinci sipariş kaydı açıyor

Kullanıcı çift tıklıyor veya yanıt gelmeyince aynı işlemi yeniden gönderiyor. Sunucu her isteğe yeni kayıt açtığı için tek iş birden fazla siparişe dönüşüyor ve düğmeyi kapatmak bunu bütünüyle engellemiyor.

- Önem: ORTA. Etkisi orta. Trafik artınca ya da istek tekrarlanınca tetiklenir.
- Önem notu: Geciken yanıt ve tekrar gönderim altında ikinci taslak sipariş oluşması temel alınır. Geri alınabilir taslakta etki sınırlıdır. Aynı yol ödeme, teslim veya ücretli çağrı başlatıyorsa önem artar ve dış yan etki ayrıca korunmalıdır.
- Eksen ve kategori: Sağlamlık, 13 Eşzamanlılık ve tutarlılık
- Yığın: Node.js, Her yığın
- Yapay zekâ kodunda: ölçülmedi. Dayanak: uzman görüşü.
- Ne zaman bakılır: İlk yayından önce, Ödeme almadan önce
- CWE: CWE-841
- OWASP Top 10:2025: A06:2025 Insecure Design
- Checklist ifadesi: Aynı sipariş ya da işlem iki kez gönderilince ikinci kayıt açılmıyor.
- Son inceleme: 4 Ekim 2026, Komünite editörlüğü
- Adres: https://vibecheck.komunite.com.tr/madde/cift-gonderim-cift-kayit-olusturuyor

## 60 saniyelik kontrol

Yalnız kendi uygulamanda ya da yazılı izin aldığın sistemde dene. Bu bir sızma testi değildir.

1. Aynı yerel isteği aynı kullanıcı ve iş anahtarıyla iki kez gönder.
2. Yanıt kimliğinin aynı kaldığını ve kalıcı kayıt sayısının artmadığını doğrula.
3. Aynı anahtarla farklı içerik gönderildiğinde eski kaydın değişmediğini kontrol et.
4. Veritabanını yeniden açıp aynı işin tekrarında aynı kaydın döndüğünü doğrula.
5. Başka kullanıcı ve yeni iş anahtarının bağımsız kayıt oluşturabildiğini kontrol et.

## Ne oluyor

Kullanıcı sipariş taslağını gönderiyor. Ekran yanıtı beklerken tekrar tıklıyor veya ağ bağlantısı koptuğu için aynı formu yeniden gönderiyor. Sunucu her istekte yeni kayıt kimliği üretiyor. Kullanıcının tek işi veritabanında iki ayrı taslak olarak beliriyor. Her kayıt kendi başına geçerli olduğu için sıradan alan doğrulaması bu sorunu yakalamıyor.

Düğmeyi ilk tıklamadan sonra kapatmak kullanıcı deneyimini iyileştirir. Ancak istek başka sekmeden, yeniden yüklenen sayfadan veya otomatik ağ tekrarından gelebilir. İlk yanıt kaybolduğunda kullanıcı işin kaydedilip kaydedilmediğini bilemez. Sunucu aynı işi tanımıyorsa yeniden gönderimi yeni niyet olarak ele alır.

Bu ayrımı kayıt kimliği tek başına çözmez. Her çağrıda yeni kayıt kimliği üretmek çakışmayı azaltır ama aynı işin tekrarlandığını söylemez. İş boyunca korunan ayrı bir anahtar gerekir. Anahtarın hangi kullanıcıya ve hangi işlem türüne ait olduğu da önemlidir. Başka kullanıcının aynı metni kullanması onun kaydını döndürmemeli, aynı anahtarın farklı içerikle gelmesi ilk işi sessizce değiştirmemelidir.

## Gerçek olay

Bu maddede belirli bir ürünün çift sipariş olayı anlatılmıyor. [PostgreSQL kısıt belgeleri](https://www.postgresql.org/docs/current/ddl-constraints.html), tekilliğin bir veya birden fazla sütun üzerinde uygulanabildiğini açıklar. [SQLite UPSERT belgesi](https://www.sqlite.org/lang_upsert.html) ise tanımlı tekillik çakışmasında yeni yazının atlanmasını destekler. Örnek bu mekanizmaları aynı işin kalıcı kimliği için kullanır.

Deney disk üzerinde geçici SQLite veritabanıyla çalışır. Aynı kullanıcı, anahtar ve içerik tekrar geldiğinde iyi sürüm aynı kayıt kimliğini döndürür. Veritabanı yeniden açılınca sonuç korunur. Farklı içerik reddedilir, başka kullanıcı ve yeni iş ayrı kayıt açabilir. Bu test ödeme sağlayıcısı veya gerçek tarayıcı çift tıklaması çalıştırmaz. Sunucudaki kayıt davranışını sınar.

## Yapay zekâ bunu neden üretiyor

**Form akışı tek gönderim varsayar.** Ajan formu, doğrulamayı ve kayıt yazısını sırayla kurar. Bir kez gönderip kaydı görünce temel iş tamamlanmış sayılır. Yanıtın kaybolması veya kullanıcının aynı niyeti yeniden göndermesi farklı senaryo olarak düşünülmez. Böylece her HTTP isteği yeni bir iş kabul edilir.

**Arayüz kilidi sunucu güvencesi sanılır.** Ajan düğmeye yükleniyor durumu ekleyince ikinci tıklama görünür biçimde engellenir. Bu iyi bir arayüz davranışıdır. Ancak sunucu başka kaynaktan gelen tekrarı yine kabul eder. Görünür düğme ile kalıcı kayıt arasındaki sınır incelenmezse çözüm yalnız ilk istemcinin davranışına bağlı kalır.

**Rastgele kimlik yanlış sorunu çözer.** Ajan her istekte yeni UUID üreterek kayıtların kimlik çakışmasını önleyebilir. Bu, aynı işin tek kez oluşmasını sağlamaz. Anahtarın iş başladığında üretilip tekrar boyunca korunması gerekir. Her denemede değişen bir anahtar, tekrar tanıma görevini yerine getiremez.

**Bellek haritası yeterli görünür.** Ajan iş anahtarlarını süreç içindeki bir kümede tutabilir. Yerel deneme geçer ama yeniden başlatma veya başka uygulama örneği bu kümeyi paylaşmaz. Kalıcı tekillik doğru veri katmanında kurulmalıdır. Bunlar olası üretim nedenleridir. Belirli bir AI aracının bu arızayı üretme sıklığına ilişkin araştırma sonucu sunulmuyor.

## Etki

Kullanıcı aynı işi listede birden fazla kez görebilir. Destek ekibi hangi kaydın gerçek niyeti temsil ettiğini ayıklamak zorunda kalır. Taslakta geri alınabilir olan sorun, sonrasında teslim veya ücretli işlem başlatılıyorsa daha pahalı hale gelir. Örnek yalnız taslak oluşturur ve herhangi bir ödeme yapmaz.

Aşırı geniş tekillik de yanlış sonuç verir. Aynı üründen bilinçli olarak yeni sipariş veren kullanıcı engellenmemelidir. Aynı içerik her zaman aynı iş demek değildir. İş anahtarı kullanıcı niyetinin yaşam süresini temsil eder. Yeni niyet yeni anahtar alır, aynı işin yeniden gönderimi eski anahtarı korur. Bu sözleşme istemci ve sunucuda birlikte uygulanır.

## Nasıl anlarsın

Aynı yerel isteği aynı kullanıcı ve aynı anahtarla tekrar gönder. Yanıt kimliklerini ve kalıcı kayıt sayısını karşılaştır. İlk çağrıdan sonra yanıtı kaybetmiş gibi davranıp tekrar dene. Sunucu aynı sonucu döndürebiliyor mu? İş anahtarı yalnız logda mı bulunuyor, yoksa veritabanı kısıtına da giriyor mu?

Aynı anahtarla miktarı değiştir. İlk kayıt korunmalı ve çelişki açıkça reddedilmelidir. Başka kullanıcıya geçince eski kayda erişilmemelidir. Veritabanını yeniden açıp denemeyi tekrarla. Bellekte tutulan bir kontrol bu adımda kaybolabilir. Testte geçersiz miktarın da yeni kayıt açmadığını doğrula. Yalnız yanıtın başarılı olmasına bakmak tekrar korumasını kanıtlamaz.

## Nasıl düzeltirsin

1. **İş anahtarını koru.** İstemci anahtarı gönderim başlamadan üretip iş sonuçlanana kadar saklasın. Ağ tekrarı aynı anahtarı kullansın. Kullanıcının bilinçli yeni işi ayrı anahtar alsın. Anahtarın uzunluğunu ve biçimini sunucuda doğrula.
2. **Kapsamı sunucuda kur.** Kullanıcı kimliğini doğrulanmış oturumdan al. Örneğin tablosu yalnız taslak oluşturma işlemini tuttuğu için işlem türü örtük olarak ayrıdır. Ortak anahtar tablosunda işlem türünü de kapsama kat. Kullanıcının gönderdiği kimliğe güvenme.
3. **Tekilliği kalıcı uygula.** İyi örnek kullanıcı ve anahtar için veritabanı kısıtı kurar. Çakışan ekleme atlanır ve mevcut kayıt okunur. Önce bakıp sonra koşulsuz ekleme yapmak yeni bir yarış penceresi bırakabilir. Kısıt aynı anda gelen yazılarda da veri katmanının kuralıdır.
4. **İçeriği ve sonucu eşle.** Örnek seçilmiş alanları sabit sırayla serileştirir. Aynı anahtar farklı içerikle gelirse eski kayıt değiştirilmez. Dış ödeme veya bildirim eklenecekse o etkinin tekrar güvenliği ayrıca kurulmalıdır. Yalnız taslak kısıtı dış serviste tek işlem garantisi vermez.

## Bir daha olmasın

Çift gönderim ve kayıp yanıt senaryolarını kayıt oluşturma testlerine ekle. Anahtar saklama süresini, silinmesini ve yeni iş başlatma kararını ürün sözleşmesinde açık tut.

## Sınır

Bu madde aynı niyetin tekrarında kopya kayıt oluşmasını kapsar. Aynı stok için farklı işlerin yarışı ve çok tablolu atomiklik ayrı konulardır. Yerel test eşzamanlı süreç başlatmaz, kalıcı tekrar davranışını doğrular. Doğası gereği aynı sonucu üreten yazı veya ayrı kullanıcı niyeti otomatik kopya sayılmaz.

## Düzeltme kodları

### Node.js: Kullanıcı ve iş anahtarı tekilliği

Önce:

```js
// siparis/taslak.js, açıklama amaçlı. userId doğrulanmış sunucu oturumundan gelir.
import { randomUUID } from 'node:crypto';
import { z } from 'zod';
const inputSchema = z.strictObject({ sku: z.enum(['demo-a', 'demo-b']), quantity: z.int().min(1).max(10) });
const keySchema = z.string().regex(/^[a-z0-9-]{8,80}$/);
export function setup(db) {
  db.exec(`CREATE TABLE orders(id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
    request_key TEXT NOT NULL, payload TEXT NOT NULL);`);
}
export function createDraft(db, userId, key, input) {
  keySchema.parse(key);
  const { sku, quantity } = inputSchema.parse(input);
  const id = randomUUID();
  db.prepare('INSERT INTO orders VALUES (?, ?, ?, ?)')
    .run(id, userId, key, JSON.stringify({ sku, quantity }));
  return { id, sku, quantity };
}
// Düğme tarayıcıda kapatılsa da sunucu her tekrarda yeni kayıt açar.
```

Sonra:

```js
// siparis/taslak.js, açıklama amaçlı. userId doğrulanmış sunucu oturumundan gelir.
import { randomUUID } from 'node:crypto';
import { z } from 'zod';
const inputSchema = z.strictObject({ sku: z.enum(['demo-a', 'demo-b']), quantity: z.int().min(1).max(10) });
const keySchema = z.string().regex(/^[a-z0-9-]{8,80}$/);
export function setup(db) {
  db.exec(`CREATE TABLE orders(id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
    request_key TEXT NOT NULL, payload TEXT NOT NULL, UNIQUE(user_id, request_key));`);
}
export function createDraft(db, userId, key, input) {
  keySchema.parse(key);
  const { sku, quantity } = inputSchema.parse(input);
  const payload = JSON.stringify({ sku, quantity });
  db.prepare(`INSERT INTO orders VALUES (?, ?, ?, ?)
    ON CONFLICT(user_id, request_key) DO NOTHING`).run(randomUUID(), userId, key, payload);
  const row = db.prepare('SELECT id, payload FROM orders WHERE user_id = ? AND request_key = ?')
    .get(userId, key);
  if (row.payload !== payload) throw new Error('İş anahtarı başka içerikle kullanıldı');
  return { id: row.id, sku, quantity };
}
// Aynı işin tekrarında istemci aynı anahtarı saklar ve gönderir.
// Örnek yalnız taslak kaydı açar, ödeme veya dış servis çağırmaz.
```

Düzeltmeyi kanıtlayan test:

```js
// siparis/taslak.test.mjs, açıklama amaçlı. Diskte geçici SQLite verisi.
import test from 'node:test';
import assert from 'node:assert/strict';
import { DatabaseSync } from 'node:sqlite';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const { setup, createDraft } = await import(process.env.ORNEK_DOSYA);
test('aynı iş aynı kaydı döndürür, başka içerik ve başka kullanıcı ayrılır', () => {
  const dir = mkdtempSync(join(tmpdir(), 'vc086-'));
  const file = join(dir, 'state.sqlite');
  let db = new DatabaseSync(file);
  try {
    setup(db);
    const input = { sku: 'demo-a', quantity: 2 };
    const first = createDraft(db, 'uye-a', 'request-a', input);
    assert.deepEqual(createDraft(db, 'uye-a', 'request-a', input), first);
    assert.equal(db.prepare('SELECT count(*) AS n FROM orders').get().n, 1);
    assert.throws(() => createDraft(db, 'uye-a', 'request-a', { ...input, quantity: 3 }), /başka içerikle/);
    db.close(); db = new DatabaseSync(file);
    assert.deepEqual(createDraft(db, 'uye-a', 'request-a', input), first);
    assert.notEqual(createDraft(db, 'uye-b', 'request-a', input).id, first.id);
    assert.notEqual(createDraft(db, 'uye-a', 'request-b', input).id, first.id);
    assert.equal(db.prepare('SELECT count(*) AS n FROM orders').get().n, 3);
    assert.throws(() => createDraft(db, 'uye-a', 'request-c', { ...input, quantity: 0 }));
    assert.equal(db.prepare('SELECT count(*) AS n FROM orders').get().n, 3);
  } finally { db.close(); rmSync(dir, { recursive: true, force: true }); }
});
```

## Ajan kuralı (AGENTS.md)

```md
## Çift gönderim çift kayıt açıyor (vibecheck VC-086)
- Aynı işin tekrarında aynı kalıcı iş anahtarını kullan.
- Anahtarın kapsamını doğrulanmış kullanıcı ve işlem türüyle belirle.
- Tekilliği veritabanı kısıtıyla uygula.
- Aynı anahtar farklı içerikle gelirse eski işi değiştirmeden reddet.
- Tekrarda önceki işin sonucunu döndür.
- Dış yan etkileri de kendi tekrar güvenliğiyle koru.
```

## Kaynaklar

1. [SQLite UPSERT](https://www.sqlite.org/lang_upsert.html), SQLite
2. [PostgreSQL Constraints](https://www.postgresql.org/docs/current/ddl-constraints.html), PostgreSQL
3. [Node.js SQLite parameter binding](https://nodejs.org/api/sqlite.html), Node.js
4. [CWE-841 Improper Enforcement of Behavioral Workflow](https://cwe.mitre.org/data/definitions/841.html), MITRE

---

vibecheck · Komünite editörlüğü. Metin CC BY 4.0, prompt ve kural parçaları MIT-0. Kaynak: https://vibecheck.komunite.com.tr/madde/cift-gonderim-cift-kayit-olusturuyor
