JSON Patch در محیط تولید: اتمیک بودن، idempotency و تعارضهای همزمانی
جمعبندی: JSON Patch فقط یک قالب تغییر است؛ اتمیک بودن، idempotency و همزمانی همچنان کار شماست. پیششرطهای test، escape مسیر، rollback و هماهنگی ETag را پوشش میدهد.

جمعبندی: اول مسیرهای هدف و مجوزهای patch را اعتبارسنجی کنید، سپس با test یا If-Match اعلان همزمانی اضافه کنید؛ patch را روی یک کپی اعمال کنید تا شکست چیزی نیمهنوشته باقی نگذارد.
خطر واقعی درخواست
PATCHنوشتن فیلد اشتباه نیست. این است که باور کنید نوشتن موفق شد در حالی که بیصدا تغییر شخص دیگری را بازنویسی کردهاید. JSON Patch ابزارها را میدهد، اما طراحی ایمنی همچنان کار شماست.
۱. مرور سریع مجموعه عملیات RFC 6902
JSON Patch تغییرات را بهصورت آرایهای از عملیات توصیف میکند، با نوع رسانه application/json-patch+json. شش عملیات به ترتیب آرایه اجرا میشوند:
| عملیات | اثر | قید کلیدی |
|---|---|---|
add |
افزودن عضو یا درج عنصر آرایه | اندیس آرایه میتواند برابر طول باشد؛ - یعنی انتها |
remove |
حذف عضو یا عنصر | مسیر هدف باید وجود داشته باشد |
replace |
جایگزینی مقدار موجود | مسیر هدف باید وجود داشته باشد |
move |
جابهجایی گره | نمیتوان گره را به مسیر فرزند خودش برد |
copy |
کپی گره | مسیر منبع باید وجود داشته باشد |
test |
اعلان برابری مقدار | اگر شکست بخورد، کل patch اعمال نمیشود |
بسیاری فقط سه مورد اول را به یاد میآورند و فراموش میکنند test همان چیزی است که نوشتن کورکورانه را به نوشتن شرطی تبدیل میکند. معناشناسی کامل عملیات و مرزهای آرایه در JSON Pointer، Patch و Merge Patch پوشش داده شده است.
۲. استفاده از test برای تبدیل patch به بهروزرسانی شرطی
یک patch معمول قفل خوشبینانه با نوشتن شروع نمیشود، با اعلان نسخه شروع میشود:
{"op":"test","path":"/version","value":1}
در patch کامل بگذارید:
[
{ "op": "test", "path": "/version", "value": 1 },
{ "op": "replace", "path": "/version", "value": 2 },
{ "op": "replace", "path": "/status", "value": "paid" }
]
منطق: تأیید کن نسخه سند اکنون ۱ است، سپس به ۲ تغییرش بده. اگر درخواست دیگری از قبل نسخه را به ۲ رسانده باشد، test اول شکست میخورد، کل patch اعمال نمیشود و سرور تعارض برمیگرداند. این از بهروزرسانی گمشده جلوگیری میکند بدون معرفی قفل توزیعشده.
چند نکته طراحی:
testرا پیش از هر نوشتن بگذارید. اگر نوشتن قبلی از قبل اثر کرده، اعلان بیمعناست؛- روی شماره نسخه یا digest تولیدشده توسط سرور اعلان کنید، نه فیلد معمولی که کلاینت آزادانه تغییر میدهد؛
- نسخه را روی هر نوشتن موفق افزایش دهید؛ وگرنه دو درخواست با همان نسخه قدیمی هر دو پاس میشوند؛
valueدرtestباید دقیقاً همان نوع JSON مقدار سند را داشته باشد.1و"1"برابر نیستند.
۳. escape مسیر و آلودگی prototype
قواعد escape JSON Pointer ساده است: ~ را ~0 و / را ~1 بنویسید، و ~ را قبل از / جایگزین کنید، وگرنه escape مضاعف میگیرید.
Key "a/b" → /a~1b
Key "m~n" → /m~0n
Key "a~/b" → /a~0~1b
ریسک patchهای بیرونی معمولاً escape نیست، کلیدهای خطرناک است. در جاوااسکریپت، کلیدهایی مانند __proto__، constructor و prototype میتوانند آلودگی prototype تحریک کنند، حتی از طریق add معمولی:
[
{ "op": "add", "path": "/__proto__/polluted", "value": true }
]
از فهرست سفید استفاده کنید نه فهرست سیاه:
- اجازه دهید patch فقط فیلدهای کسبوکار را تغییر دهد، مانند
/statusیا/items/0/quantity؛ - هر
pathوfromرا در برابر عبارت باقاعده یا پیشوند مسیر اعتبارسنجی کنید و سطوح غیرمنتظره غیر از اندیس آرایه را رد کنید؛ - JSON را به ظرفهای
Object.create(null)یاMapتجزیه کنید تا اثر زنجیره prototype آلوده کم شود؛ - تعداد عملیات و اندازه بایتی هر
valueرا محدود کنید.
۴. اعمال اتمیک و rollback
RFC 6902 اجرای اتمیک میخواهد، اما اتمیک بودن یک الزام معنایی است، نه پیشفرض هر کتابخانه. کتابخانههایی مانند fast-json-patch وقتی mutateDocument برابر true است سند را درجا دگرگون میکنند و شکست ممکن است تغییرات جزئی برجا بگذارد.
import { applyPatch, validate } from 'fast-json-patch';
/**
* Apply a patch immutably and return the original document on failure.
* @param document The original document, which callers must not mutate
* @param patch The JSON Patch operation array to apply
* @returns The result, including a success flag and the new document
*/
function applySafely(document: unknown, patch: unknown[]) {
const errors = validate(patch, document);
if (errors) {
return { ok: false, document };
}
try {
const result = applyPatch(document, patch, false, false);
return { ok: true, document: result.newDocument };
} catch {
return { ok: false, document };
}
}
سه توصیه پیادهسازی:
- اول کپی عمیق یا structured-clone کنید، روی کپی اعمال کنید و مرجع را فقط پس از موفقیت جابهجا کنید؛
- به rollback خودکار کتابخانه تکیه نکنید؛ رفتار روی استثنا بین پیادهسازیها متفاوت است؛
- پیش از اجرا اعتبارسنجی کنید:
validateفقط ساختار را بررسی میکند، در حالی که تعارضهای معنایی همچنانtestمیخواهند؛ هیچکدام جای دیگری را نمیگیرد.
اگر patch چند منبع را در بر میگیرد، مثلاً تغییر همزمان سفارش و موجودیاش، اتمیک بودن JSON Patch فقط داخل یک سند برقرار است. در بین اسناد به تراکنش پایگاهداده یا اقدامات جبرانی نیاز دارید.
۵. Idempotency: PATCH بهطور طبیعی idempotent نیست
معناشناسی HTTP PUT و DELETE را idempotent تعریف میکند، اما PATCH لزوماً چنین نیست. تکرار همان JSON Patch میتواند نتایج کاملاً متفاوتی بدهد:
add /items/-هر بار به انتهای آرایه ضمیمه میکند، پس تلاش مجدد تکراری میسازد؛replace /versionنسخه ۱ را به ۲ تغییر میدهد و در تلاش مجدد دوباره موفق میشود، حتی اگر عملیات از قبل اعمال شده باشد؛remove /tempدر اجرای دوم شکست میخورد چون مسیر دیگر وجود ندارد.
سه راه رایج برای دستیابی به idempotency:
| رویکرد | چگونه کار میکند | مناسب برای |
|---|---|---|
| پیششرط نسخه | عملیات اول test /version، سپس افزایش |
منابع با نسخه صریح |
| کلید idempotency | کلاینت Idempotency-Key میفرستد؛ سرور نتیجه را کش میکند |
پرداخت، سفارش و نوشتنهای دیگر |
| patch حالت هدف | از replace بهجای add استفاده کنید تا نتیجه فقط به مقدار نهایی بستگی داشته باشد |
فیلدهای پیکربندی و وضعیت |
بدترین الگو patchی است که add و remove را مخلوط میکند و تلاش مجدد را ایمن فرض میکند. اگر patch چیزی ضمیمه یا حذف میکند، باید با اعلان نسخه یا کلید idempotency جفت شود.
۶. تعارضهای همزمانی: ETag و If-Match
علاوه بر test داخل patch، HTTP مکانیسم استانداردی میدهد: سرور ETag برمیگرداند و کلاینت در درخواست بعدی If-Match میفرستد.
GET /orders/42 HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
ETag: "a3f9c1"
Content-Type: application/json
PATCH /orders/42 HTTP/1.1
Host: api.example.com
If-Match: "a3f9c1"
Content-Type: application/json-patch+json
[{"op":"replace","path":"/status","value":"paid"}]
اگر منبع تغییر کرده باشد، سرور 412 Precondition Failed برمیگرداند و کلاینت پیش از تلاش مجدد دوباره میگیرد. If-Match: * یعنی «تا وقتی منبع وجود دارد اجرا کن»، که وقتی باید از بازنویسی جلوگیری کنید مناسب نیست.
تفاوت test و If-Match:
If-Matchدر لایه HTTP پیش از تجزیه بدنه بررسی میشود، پس ارزانتر است؛testدر لایه معناشناسی patch بررسی میشود و میتواند هر فیلدی را بدون وابستگی به پشتیبانی ETag سرور اعلان کند؛- میتوانند ترکیب شوند، اما فقط یکی را پیاده نکنید و ادعا کنید endpoint زیر همزمانی امن است.
۷. تفاوت کتابخانهها و انتخاب
| پیادهسازی | زبان | اتمیک بودن | پشتیبانی test |
یادداشت |
|---|---|---|---|---|
fast-json-patch |
JavaScript | اختیاری (بهصورت پیشفرض سند جدید برمیگرداند) | بله | اکوسیستم گسترده؛ به mutateDocument توجه کنید |
jsonpatch |
Python | روی استثنا rollback میکند | بله | نزدیک به استاندارد، با خطاهای واضح |
json-patch |
Go | روی کپی کار میکند | بله | غالباً همراه JSON Merge Patch استفاده میشود |
zjsonpatch |
Java | بله | بله | یکپارچهسازی راحت با Jackson |
کتابخانه را فقط بر اساس «همه شش عملیات را پشتیبانی میکند» انتخاب نکنید. سه چیز را تأیید کنید: آیا شکست rollback میکند، آیا test سختگیرانه مقایسه میکند و آیا add آرایه با اندیس بیرون از بازه خطا میدهد. پیادهسازیهای سهلگیر همان patch را در محیطهای مختلف متفاوت رفتار میکنند.
۸. چکلیست ایمنی پیش از انتشار
- [ ] endpoint نوع
application/json-patch+jsonرا اعلام میکند؛ - [ ] patchها روی کپی اعمال میشوند و شکست هرگز سند اصلی را آلوده نمیکند؛
- [ ] هر
pathوfromدر patchهای بیرونی از بررسی فهرست سفید عبور میکند؛ - [ ] کلیدهای خطرناک مانند
__proto__،constructorوprototypeرد میشوند؛ - [ ] تعداد عملیات و اندازه مقدار واحد محدود است؛
- [ ]
testیاIf-Matchکنترل همزمانی میدهد؛ - [ ] patchها با کنشگر، مهر زمانی و آرایه کامل عملیات در لاگ حسابرسی نوشته میشوند؛
- [ ] مرزهای تراکنش وقتی patch چند سند را در بر میگیرد صریحاند.
patch ذاتاً توصیف کامل «چه چیزی تغییر کرد» است، که آن را رکورد حسابرسی طبیعی میکند. برای کدهای خطا و idempotency در لایه API، طراحی API در JSON را ادامه دهید. اگر patch از ادغام دو شاخه میآید، به معناشناسی تعارض هم نیاز دارید که در ادغام Git برای فایلهای JSON پوشش داده شده است.
پرسشهای متداول
عملیات test شکستخورده چه کد وضعیتی برگرداند؟
اگر تعارض از اعلان نسخه میآید، هم 409 Conflict و هم 412 Precondition Failed قابل دفاعاند. 412 را برای همخوانی با If-Match ترجیح دهید و نسخه فعلی را در بدنه پاسخ بگذارید تا کلاینت بتواند تلاش مجدد کند.
آیا patchی حاوی add و remove میتواند ایمن تکرار شود؟
مستقیم نه. add به انتهای آرایه داده را تکراری میکند و remove در اجرای دوم شکست میخورد چون مسیر رفته است. پیششرط نسخه یا کلید idempotency اضافه کنید تا تلاشهای مجدد بهعنوان همان عملیات شناخته شوند.
آیا JSON Patch میتواند تراکنش بین اسناد را تضمین کند؟
نه. اتمیک بودن RFC 6902 به یک سند JSON واحد اعمال میشود. وقتی سفارش، موجودی یا منابع دیگر دخیلاند، از تراکنش پایگاهداده، جبران Saga یا صف پیام برای یکدستی استفاده کنید.
چرا patch هرگز نباید به __proto__ بنویسد؟
چون بیشتر پیادهسازیهای جاوااسکریپت سند را در اشیای معمولی نگه میدارند، نوشتن __proto__ میتواند زنجیره prototype را تغییر دهد و روی هر شیء بعدی اثر بگذارد. فهرست سفید مسیر قابل اعتمادترین دفاع است؛ فقط به فیلتر داخلی کتابخانه تکیه نکنید.
چگونه یک درخواست PATCH را idempotent کنم؟
سه رویکرد رایج: patch را با test /version شروع کنید و نسخه را روی موفقیت افزایش دهید، سرور نتیجه را برای Idempotency-Key کش کند، یا از replace بهجای add استفاده کنید تا نتیجه فقط به مقدار نهایی بستگی داشته باشد. اگر patch چیزی ضمیمه یا حذف میکند، باید با اعلان نسخه یا کلید idempotency جفت شود؛ نمیتوان صرفاً تکرارش کرد.
آیا باید هم test و هم If-Match استفاده کنم؟
لازم نیست هر دو را استفاده کنید، اما پیاده کردن فقط یکی endpoint را زیر همزمانی امن نمیکند. If-Match در لایه HTTP پیش از تجزیه بدنه بررسی میشود، پس ارزانتر است؛ test در لایه معناشناسی patch بررسی میشود و میتواند هر فیلدی را بدون وابستگی به پشتیبانی ETag سرور اعلان کند.