وب مدرن، رابط کاربری و تجربهی وب · فصل ۵ — Error Model، REST، Streaming و Webhook · 48
Error Model: Domain/Public/Retryable Errors
میتواند «Error Model: Domain/Public/Retryable Errors» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
خلاصهٔ درس
میتواند «Error Model: Domain/Public/Retryable Errors» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
خروجی این درس
میتواند «Error Model: Domain/Public/Retryable Errors» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
پیشنیازها
پاسخ 500 قرارداد Retry نیست
فرض کنید یک command پرداخت به سرویس پاییندستی ارسال شده است. ممکن است provider تراکنش را اعمال کرده باشد، اما connection قبل از دریافت پاسخ قطع شود. در این لحظه سیستم نمیداند side effect اتفاق افتاده یا نه. اگر backend این وضعیت را به یک 500 عمومی تبدیل کند، raw exception را در response بگذارد و client هم همه 5xxها را دوباره ارسال کند، یک failure فنی میتواند به تکرار یک عمل تجاری تبدیل شود. Shortcut مقابل هم ضعیف است: همه خطاها را به یک پیام مبهم تبدیل کنیم و هیچ چیز را retryable ندانیم. در این حالت client مجبور میشود recovery را حدس بزند.
WEB-004 سه تصمیم را از هم جدا میکند: در Domain چه چیزی شکست خورده است، Public API چه چیزی مجاز است نشان دهد، و آیا همین operation در همین شرایط قابل replay است یا نه. این سه تصمیم به هم مرتبطاند، اما یک data structure واحد نیستند. خروجی درس Error Boundary & Retryability Record v1 است؛ رکوردی که یک مسیر retry-safe، یک مسیر retry-unsafe، یک mutation عمدی، repair، و یک transfer مستقل را با evidence قابل بازرسی ثبت میکند.
قانون اصلی این درس این است: status code evidence است، نه retry policy کامل. Retry-After میتواند timing مناسب برای درخواست بعدی را نشان دهد، اما یک command غیر-idempotent با outcome مبهم را خودکار به replay امن تبدیل نمیکند. Problem Details هم یک representation استاندارد و مفید است، نه اجبار برای تبدیل Domain Model به payload HTTP.
محدوده، مسیر و معیار Evidence
این درس روی HTTP/message basics از WEBF-009 بنا میشود. مالک error taxonomy، مرز mapping از internal به public و تصمیم محدود retryability است. معماری Fastify، طراحی کامل Idempotency-Key، pagination، SSE و webhook reliability خارج از scope هستند. وقتی امنبودن replay به یک idempotency contract وابسته است، WEB-004 فقط آن precondition را ثبت میکند و در نبود آن retry را رد میکند؛ طراحی کامل آن contract در WEB-006 انجام میشود.
اگر Domain Error، HTTP Error و retry logic در یک catch block مخلوط شدهاند، مسیر foundation را بروید. اگر public error code دارید اما client هنوز از 5xx نتیجه retry میگیرد، مسیر repair مناسب است. اگر تجربه دارید، syntax را فشرده کنید و مستقیم سراغ boundary review، ambiguous outcome و کیفیت evidence بروید. پایان هر سه مسیر یکسان است: behavior مشاهدهشده، inference صریح، mutation/repair، transfer مستقل و residual limitation.
Lab محلی، deterministic و non-destructive است. بهجای payment provider واقعی از fake downstream استفاده میکند. هیچ browser یا assistive-technology observation در این درس لازم یا ادعا نمیشود. Evidence قابل مشاهده شامل status، public body، Correlation ID، retry decision، attempt count و fake side-effect count است.
Mental Model: failure source → domain meaning → public contract → HTTP semantics → retry decision
این مسیر را چند boundary جدا ببینید، نه یک exception handler بزرگ.
- Failure Source: اتفاق واقعی را نامگذاری کنید؛ input نامعتبر، insufficient funds، dependency failure قبل از side effect، یا timeout مبهم بعد از احتمال side effect.
- Domain Classification: معنای business/operational را بدون transport field ثبت کنید.
InsufficientFundsذاتاً409نیست وPaymentOutcomeUnknownذاتاً503نیست. - Public Mapping: تعیین کنید caller چه code پایدار و چه detail امنی ببیند. Correlation evidence را نگه دارید، اما stack trace، SQL fragment، provider secret یا raw exception را بیرون ندهید.
- HTTP Semantics: method/status/header را با معنای public هماهنگ کنید. HTTP semantics interface را محدود میکند، اما جای domain taxonomy را نمیگیرد.
- Retry Preconditions: بررسی کنید repetition چه اثری دارد، outcome درخواست اول معلوم است یا نه، replay-safety contract وجود دارد یا نه، و failure احتمال recovery زمانی دارد یا نه.
- Client Decision: auto-retry، terminal error یا stop-and-reconcile.
- Evidence + Residual Risk: observation، inference و عدم قطعیت باقیمانده را جدا ثبت کنید.
Dangerous shortcut همینجاست: از «status=503» مستقیم به «retry امن است» نپرید. Recovery timing و replay safety دو سؤال متفاوتاند.
Foundation: پنج قرارداد مستقل
۱. Domain Error Contract — WEB-004-K1
Domain Error معنایی را نمایش میدهد که به business یا bounded application domain تعلق دارد. باید بدون دانستن HTTP، queue، CLI یا in-process call قابل فهم باشد. در این درس نمونههایی مثل ValidationFailure، InsufficientFunds، DependencyUnavailable و PaymentOutcomeUnknown داریم.
Transport field را وارد Domain Type نکنید. این جداسازی دو مزیت دارد: اول اینکه همان meaning در boundary دیگری میتواند mapping متفاوتی داشته باشد. دوم اینکه handler نمیتواند convention حملونقل را به business truth تبدیل کند. Domain Error میتواند code داخلی پایدار و context محدود داشته باشد، اما برای وجود خود به status، Retry-After یا application/problem+json نیاز ندارد.
این اصل به معنی ساخت inheritance tree بزرگ نیست. یک tagged value کوچک هم کافی است، اگر meaning را قبل از serialization حفظ کند.
۲. Public Error Contract — WEB-004-K2
Public Error یک compatibility surface است. Client ممکن است بر اساس code پایدار branch بزند، پیام امن نشان دهد، Correlation ID را برای support نگه دارد یا از recovery metadata مستند استفاده کند. بنابراین public mapping تصمیم API-design است، نه repr(error).
Mapping باید عمداً private detail را حذف کند. Provider hostname، stack frame، SQL text یا internal account identifier ممکن است در protected log مفید باشند، اما نباید contract response شوند. فقط حذف stack trace هم security کامل نمیسازد، ولی public response نباید به internals خصوصی وابسته باشد.
Failureهای materially different را وقتی caller action متفاوتی نیاز دارند با هم collapse نکنید. insufficient_funds و dependency_unavailable هر دو failure هستند، اما اولی معمولاً نیاز به تغییر state توسط کاربر دارد و دومی ممکن است بعداً recover شود. اگر هر دو به operation_failed تبدیل شوند، information لازم برای recovery امن از بین میرود.
۳. HTTP Semantics Contract — WEB-004-K3
Status و method semantics معنای مشترک protocol را فراهم میکنند. اینها ورودی public design هستند، نه replacement برای Domain Model. 409، 422، 503 یا 500 میتوانند semantic عمومی response را منتقل کنند و public error code معنای application-specific را کامل کند.
RFC 9110 propertyهای safe و idempotent method را تعریف میکند و توضیح میدهد چرا در برخی communication failureها request idempotent میتواند دوباره ارسال شود. همان استاندارد هشدار میدهد non-idempotent request را بدون دلیل مستقل برای replay-safe بودن یا دانستن اینکه request قبلی اعمال نشده، خودکار retry نکنید. این distinction از «status با چه عددی شروع شده» مهمتر است.
Method هم بهتنهایی کافی نیست. GETای که پنهانی action unsafe انجام میدهد contract غلطی دارد. POST ممکن است با یک application contract جدا replay-safe شود، اما WEB-004 آن protocol را کامل آموزش نمیدهد؛ فقط precondition را ثبت میکند.
۴. Retryability Contract — WEB-004-K4
Retryability تصمیمی مشروط بر حداقل چهار بعد است: operation semantics، failure class، outcome certainty و recovery timing.
- Operation Semantics: repetition همان intended effect را حفظ میکند یا command را duplicate میکند؟
- Failure Class: transient، terminal، validation-related یا ambiguous؟
- Outcome Certainty: میدانیم request اعمال نشده یا side effect ممکن است وجود داشته باشد؟
- Recovery Timing: evidence داریم که صبر کردن کمک میکند، مثلاً backoff یا
Retry-After؟
مسیر retry-safe میتواند یک operation read-like/idempotent باشد که بهدلیل dependency transient قبل از نتیجه fail شده است. مسیر retry-unsafe نمونه پرداختی POST است که بعد از احتمال side effect timeout شده و replay-safety/reconciliation contract ندارد. در حالت دوم auto-replay باید متوقف شود و public state صریح «outcome unknown» بدهد.
Retry-After میگوید «چه زمانی درخواست دوباره ممکن است مناسب باشد؟» اما نمیگوید «تکرار این business operation امن است؟» در Evidence Record این دو فیلد جدا هستند.
۵. Problem Details Contract — WEB-004-K5
RFC 9457 یک representation machine-readable مشترک برای Problem Details در HTTP API تعریف میکند. برای APIهایی مفید است که shape استاندارد با problem type، title، status، detail، instance و extensionهای کنترلشده میخواهند. همان استاندارد جا برای formatهای domain-specific موجود باقی میگذارد و استفاده از application/problem+json را برای همه خطاها اجباری نمیکند.
بنابراین fixture این درس دو representation قابل قبول دارد: Problem Details-like response و یک domain envelope کوچک. هر دو باید همان public meaning و HTTP semantics را حفظ کنند. انتخاب representation، failure را retryable نمیکند و نباید status semantics را بازتعریف کند.
Type identifier باید پایدار باشد، نه error message پویا. Client نباید از parsing متن انسانی نتیجه retry بگیرد.
توضیح عمیق: Ambiguity خودش یک State است
سختترین failure همیشه exception بزرگتر نیست؛ گاهی failure دانستههای سیستم درباره side effect را از بین میبرد.
در payment command، API درخواست را به provider میفرستد. Provider command را میپذیرد، اما connection قبل از confirmation قطع میشود. از نگاه API دو جهان ممکن است: charge اتفاق افتاده یا نیفتاده است. Generic 500 این فرق را پاک میکند. Retry کورکورانه ممکن است effect را تکرار کند. Declaring permanent failure هم ممکن است غلط باشد، چون action اول شاید موفق شده باشد.
مدل بهتر uncertainty را به state صریحی مثل payment_outcome_unknown تبدیل میکند. Public response ممکن است در کلاس server failure باشد، اما retry decision همچنان false بماند. قدم بعدی reconciliation یا recovery flow محصول است، نه حدس.
به همین دلیل «transient transport error» و «retryable business operation» مترادف نیستند. Transport شاید دو ثانیه بعد سالم شود، اما business operation هنوز unsafe برای repetition باشد.
Correlation ID evidence عملیاتی را حفظ میکند بدون اینکه domain meaning شود. Identifier باید از mapping عبور کند تا client بتواند آن را گزارش کند و protected telemetry event مرتبط را پیدا کند. Presence آن بهتنهایی correctness یا security را اثبات نمیکند.
Worked Example: Payment-like Command
Fixture چهار outcome اصلی دارد.
Validation Failure: request از نظر contract قابل قبول نیست. Domain validation_failure میدهد، public response یک client-error status و code پایدار دارد. تا وقتی caller request را تغییر ندهد retry=false است.
Insufficient Funds: business rule به شکل deterministic fail میشود. Public response code پایدار insufficient_funds را نگه میدارد و object داخلی provider را بیرون نمیدهد. گذشت زمان بهتنهایی دلیل auto-retry نیست.
Dependency Unavailable Before Effect: fake downstream قبل از side effect fail میشود. برای request read-like/idempotent، retry پس از backoff میتواند مجاز باشد. برای side-effecting POST بدون replay-safety contract، auto-retry مجاز نیست. Retry-After timing evidence است، نه proof safety.
Ambiguous Timeout After Possible Effect: fake downstream side-effect counter را افزایش میدهد و بعد ambiguous outcome میدهد. Public mapping از payment_outcome_unknown استفاده میکند و auto-retry=false میماند. Evidence روشن است: یک effect ممکن است از قبل وجود داشته باشد و mutation retry میتواند duplicate بسازد.
نکته مهم این است که دو failure میتوانند هر دو server-error status داشته باشند، اما retry decision متفاوت باشد. Status معنای خود را دارد، ولی client decision evidence بیشتری میخواهد.
Failure Modes و Mutationهای عمدی
Mutation A — Raw Exception Leak
Mapper را طوری خراب کنید که repr(error) را serialize کند. پیشبینی: private implementation detail از public boundary عبور میکند و response shape به internals وصل میشود. Repair باید stable code، safe detail و Correlation ID را برگرداند و diagnostics خصوصی را بیرون response نگه دارد.
Mutation B — Every 5xx Is Retryable
Policy را به status >= 500 تبدیل کنید. Ambiguous payment باید اشتباه replay شود و fake side-effect count مشکل را نشان دهد. Repair: قبل از retry، operation semantics و outcome certainty را بررسی کنید.
Mutation C — Retry-After Means Safe
Policy را طوری تغییر دهید که وجود Retry-After خودکار retry کند. این mutation برای command غیر-idempotent با outcome unknown باید fail شود. Repair: header فقط بعد از ثابت شدن replay safety بهعنوان timing استفاده شود.
Mutation D — Collapse Public States
insufficient_funds و dependency_unavailable را به یک code و client action یکسان تبدیل کنید. Public contract distinction لازم برای recovery را از دست میدهد. Repair: categoryهای پایدار و materially different را جدا نگه دارید.
Experienced Bridge: Decision Table را Review کنید، نه Catch Block را
برای مهندس باتجربه سریعترین مسیر matrix است: Domain Class، Public Code، Status، private fieldهای حذفشده، outcome certainty، replay-safety precondition، retry timing، client action و residual uncertainty. اگر در یک row از exception مستقیم به retry پریدهاید، مدل ناقص است.
این جداسازی discussion معماری را دقیقتر میکند. تیم ممکن است درباره 409 یا 422 اختلاف داشته باشد، اما همزمان توافق کند که Domain Error نباید HTTP field داشته باشد و ambiguous side effect نباید auto-replay شود. Durable boundary را از representation قابل مذاکره جدا کنید.
Matrix را به taxonomy جهانی همه سرویسها تبدیل نکنید. Error Model قرارداد محصول است. Principleها را reuse کنید، اما public code و recovery behavior را به API واقعی محدود نگه دارید.
Alternatives و Trade-offs
Problem Details در برابر Domain Envelope موجود: Problem Details structure استاندارد HTTP-facing و extension model میدهد. API قدیمی ممکن است envelope سازگار خود را داشته باشد. هر دو وقتی قابل قبولاند که status semantics درست بماند، machine-readable code پایدار باشد، private detail خارج شود و client از prose نتیجه retry نگیرد.
Boolean retryable در برابر Recovery Decision غنیتر: boolean ساده است، اما timing، ambiguity، maximum attempts یا reconciliation need را مخفی میکند. Fixture درس برای کوچک ماندن از boolean استفاده میکند و rationale را جدا ثبت میکند. Production ممکن است metadata غنیتر بخواهد.
Server Hint در برابر Client Policy: server معمولاً failure class و recovery timing را بهتر میداند؛ client operation context و replay contract را میداند. Design قوی این دو را ترکیب میکند و هیچکدام را omniscient فرض نمیکند.
Aggressive Retry در برابر Conservative Reconciliation: auto-retry در operation امن resilience را زیاد میکند. stop-and-reconcile در ambiguous side effect از duplicate محافظت میکند. انتخاب درست تابع contract operation است، نه سلیقه «همیشه retry» یا «هیچوقت retry نکن».
تمرین هدایتشده و Transfer مستقل
در guided task قبل از edit، broken fixture را inspect کنید. برای هر boundary یک جمله بنویسید: domain meaning، public response، HTTP semantics، retry precondition و client action. چهار failure probe را predict کنید. بعد کوچکترین repair را انجام دهید که behavior مشاهدهشده را با contract هماهنگ کند.
Evidence را به observation و inference بشکنید. «status برابر 503 بود و attempts برابر 1 ماند» observation است. «auto-replay بهدرستی متوقف شد چون outcome ambiguous بود و replay-safety contract وجود نداشت» inference است. این جداسازی اجازه نمیدهد green checker جای reasoning را بگیرد.
در transfer مستقل همان روش را برای notification-delivery command اجرا کنید. Provider خارجی ممکن است notification را پذیرفته باشد و بعد connection fail شود. Payment-specific code را copy نکنید. Domain meaning، public code، status و retry decision مخصوص context جدید را بنویسید. مشخص کنید قبل از auto-retry چه چیزی لازم است: reconciliation، provider receipt یا replay-safety contract.
Verification: Error Boundary & Retryability Record v1 چه چیزی را باید ثابت کند
Record فقط وقتی کامل است که این موارد را داشته باشد:
- implementation artifact مربوط به domain/public mapping؛
- behavior مشاهدهشده برای حداقل یک terminal case، یک retry-safe case و یک ambiguous retry-unsafe case؛
- Correlation ID که از public boundary عبور کرده است؛
- prediction و result یک deliberate mutation؛
- repair result؛
- transfer مستقل در context جدید؛
- جداسازی observation و inference؛
- حداقل یک residual limitation.
Green validator فقط ثابت میکند bounded fixture checkهای encodeشده را پاس کرده است. ثابت نمیکند status choice برای تمام APIها ایدهآل است، integration واقعی payment امن است یا security review کامل انجام شده است. در این fixture API-only هیچ browser/AT evidence ادعا نمیشود.
Assessment و Transfer
- Retrieval: پنج contract و یک non-goal را بدون نگاه به matrix بیان کنید.
- Application: guided fixture را repair و Evidence Record را کامل کنید.
- Diagnosis: یک mutation را قبل از اجرا predict، سپس detect و diagnose کنید.
- Decision: یک mapping/retry choice را در برابر alternative قابل قبول دفاع کنید و evidence تعیینکننده را نام ببرید.
- Transfer: ambiguity مربوط به notification delivery را مستقل حل کنید و بگویید چه چیزی هنوز ثابت نشده است.
پاسخ قوی فقط واژههای idempotent یا Problem Details را تکرار نمیکند؛ term را به operation، failure class، public compatibility surface و observed evidence وصل میکند.
References و Freshness
RFC 9457 — Problem Details for HTTP APIs: برای claim محدود درباره format مشترک machine-readable در HTTP error و این fact که استاندارد جایگزین اجباری همه representationهای domain-specific نیست. در 2026-09-13 دوباره بررسی شد؛ review due: 2026-10-17.
RFC 9110 — HTTP Semantics: برای safe/idempotent method semantics، status semantics و claim محدود retry: non-idempotent request بدون دلیل مستقل برای replay safety یا اثبات applied نشدن request قبلی نباید خودکار تکرار شود. در 2026-09-13 دوباره بررسی شد؛ review due: 2026-10-17.
روش پایدار درس boundary separation و evidence discipline است. Mapping دقیق هر محصول design decision باقی میماند. اگر guidance استاندارد تغییر کرد، claim متاثر را narrow/update کنید و scope درس را بیصدا گسترش ندهید.
ارزیابی
پاسخنامهی این درس در بستهی آموزش وجود ندارد؛ خودت جواب بده و صادقانه علامت بزن. «نیاز به مرور» درس را به صف تکرار میفرستد.
WEB-004-S1 retrieval
پنج قرارداد WEB-004 و یک non-goal صریح را بیان کنید.
WEB-004-S2 application
fixture هدایتشده API را اصلاح کنید تا domain classification، public mapping و retry decision از هم جدا و مبتنی بر evidence باشند.
WEB-004-S3 diagnosis
یک mutation را که باعث نشت اطلاعات داخلی یا تکرار یک دستور side-effecting میشود پیشبینی، اجرا، تشخیص و اصلاح کنید.
WEB-004-S4 decision
با تکیه بر semantics عملیات و قطعیت outcome، از یک تصمیم HTTP/public-error/retry در برابر یک گزینه جایگزین قابلقبول دفاع کنید.
WEB-004-S5 transfer
یک ambiguous notification-delivery outcome را مستقل مدل کنید و مشخص کنید پیش از توجیه automatic retry چه چیزی باید معلوم باشد.
ملاکهای ارزیابی (rubric)
Critical dimensions:
- Domain Error Boundary — Business/domain failures remain transport-agnostic; HTTP status, headers, correlation IDs and retry decisions are mapped outside the domain error object.
- Public Error Contract — Public codes/messages are stable, distinct where client decisions differ, preserve correlation, and do not expose internal implementation details.
- HTTP Semantics — Status/method semantics inform the mapping but are not treated as the complete domain model.
- Retryability Judgment — Retry depends on operation semantics, failure class, outcome certainty and an explicit replay-safety basis;
Retry-Afteralone is not replay-safety proof. - Problem Details & Transfer — RFC 9457 representation is used only where appropriate; the independent notification-delivery transfer preserves changed-context semantics, evidence coherence and one residual limitation.
Blocking assessments: S2, S3, S5. S1 and S4 are non-blocking but still require scope discipline. Machine checks do not fabricate browser/API evidence; evidence claims must match actually executed fixture behavior.
منابع و تازگی
مطالعهٔ موارد (case studies)
فایلهای آزمایشگاه
Error Model خراب را repair کنید. Domain meaning را از transport جدا نگه دارید، Public Contract پایدار و امن بسازید و Retry Decision را بر اساس operation semantics، failure class، outcome certainty و recovery timing بگیرید. Idempotency-Key protocol کامل پیادهسازی نکنید. Evidence Record و transfer مستقل notification-delivery را کامل کنید.
رسانه و اسلایدهای درس
فایلهای آموزشی (دانلود)