IlmHamroh
JavaScript Full-stack/25-qism. HTTP, Express va REST API23/51-dars24 daqiqa
Mundarija (35)

API xato javoblari: Problem Details (RFC 9457) — type, title, status, detail

Qisqacha: API'ning har bir xatosi bir xil shaklda qaytishi kerak — shunda mijoz uni bitta funksiya bilan o'qiydi. Buning standarti Problem Details (RFC 9457, 2023; eski RFC 7807 o'rnini oldi): Content-Type: application/problem+json va tanada type, title, status, detail, instance maydonlari. Umumiy HTTP xatosiga type: "about:blank", o'z domen xatolaringizga esa katalogdagi URI beriladi. 500 xatoda ichki tafsilot (stack, parol, SQL) javobga hech qachon chiqmaydi — u faqat logga yoziladi.

Bu darsda

  • Nega "har kim o'zicha" xato formati mijozni qiynashini tushuntira olasiz.
  • RFC 9457 maydonlarini (type, title, status, detail, instance) va kengaytmalarni to'g'ri ishlatasiz.
  • Validatsiya xatolarini massiv bilan, har maydon uchun alohida qaytarasiz.
  • Xato kodlari katalogini tuzasiz va mijoz xatoni matn bo'yicha emas, kod bo'yicha ajratishini ta'minlaysiz.
  • vazifalar-api dagi barcha xatolarni Problem Details'ga o'tkazasiz.

Oldin bilishingiz kerak: Status kodlarni to'g'ri tanlash, Middleware va request lifecycle, Pagination: offset, cursor, keyset, Stack trace, source map va error.cause.

1. Nega bu kerak?

vazifalar-api hozir xatolarni turli shaklda qaytaradi. O'zimiz yozgan xatolar — { "xato": "…" }. Express o'zi topmagan manzilga HTML sahifa beradi: Cannot PUT /teglar. express.json() buzuq JSON'da yana boshqa shakl chiqaradi. Mijoz (frontend, mobil ilova, boshqa server) har birini alohida tanishi kerak.

Endi tasavvur qiling: Malika Click'ga o'xshash to'lov ilovasini yozyapti va uchta bankning API'siga ulanadi. Birinchi bank xatoni { "error": "…" } da, ikkinchisi { "message": "…", "code": 12 } da, uchinchisi esa 200 status bilan { "success": false } da qaytaradi. Malika uchta xato o'quvchi yozadi va har birini alohida sinaydi.

O'xshatish: pochtadagi "qaytarilgan xat" blanki. Xat qaysi shahardan bo'lishidan qat'i nazar, blank bir xil: "sabab", "izoh", "sana" kataklari bor. Pochtachi blankni bir qarashda o'qiydi. Problem Details — HTTP API uchun aynan shunday umumiy blank.

2. Problem Details nima?

2.1 Standart va uning tarixi

Problem Details (muammo tafsiloti) — HTTP API xato javobining standart JSON shakli. U IETF'ning RFC hujjatida yozilgan:

Hujjat Yil Holati
RFC 7807 2016 eskirgan — RFC 9457 o'rnini oldi
RFC 9457 2023 (iyul) amaldagi standart

RFC 9457 maydonlarni o'zgartirmadi. U ko'p xatoni bitta javobda qaytarish misolini, type URI'lar uchun umumiy ro'yxatni (IANA registry) va aniqroq qoidalarni qo'shdi. Shuning uchun eski kodda "RFC 7807" deb yozilgan bo'lsa — format o'sha, faqat hujjat raqami yangilangan.

Javob ikki belgidan tanib olinadi:

http
HTTP/1.1 404 Not Found
Content-Type: application/problem+json

{
  "type": "https://vazifalar-api.example/xatolar/vazifa-topilmadi",
  "title": "Vazifa topilmadi",
  "status": 404,
  "detail": "Vazifa 99 topilmadi",
  "instance": "/vazifalar/99"
}

Birinchisi — media turi application/problem+json (oddiy application/json emas). Ikkinchisi — tanadagi maydonlar.

2.2 Beshta maydon

Maydon Nima Kim uchun
type xato TURINI bildiruvchi URI mashina (dastur)
title tur uchun qisqa sarlavha — har safar bir xil odam
status HTTP status kodi (javob statusi bilan bir xil) ikkalasi
detail aynan SHU holat haqida tushuntirish odam
instance aynan shu holatning manzili (qaysi so'rovda) qo'llab-quvvatlash xizmati

Eng muhim farq — title va detail orasida. title tur bilan bog'langan: "Vazifa topilmadi" — 7-vazifa ham, 99-vazifa ham shu sarlavhani oladi. detail esa shu holatga xos: "Vazifa 99 topilmadi". Mijoz ekranda detail ni ko'rsatadi, statistikani esa type bo'yicha yig'adi.

Hamma maydon ixtiyoriy, lekin amalda type, title, status doim bo'ladi. type berilmasa, uning qiymati "about:blank" deb hisoblanadi.

2.3 about:blank — "oddiy HTTP xatosi"

Ba'zi xatolar sizning domeningizga xos emas: manzil yo'q (404), metod noto'g'ri (405), tana juda katta (413). Ular uchun alohida tur o'ylab topish shart emas. RFC 9457 qoidasi: type: "about:blank" bo'lsa, title — status kodining standart matni ("Not Found", "Method Not Allowed"). Ya'ni bu xato status kodidan boshqa hech narsa aytmaydi.

about:blank — brauzerdagi bo'sh sahifa manzili. Standart uni "bu turda qo'shimcha ma'no yo'q" degan belgi sifatida tanlagan.

2.4 type — URI, lekin ochilishi shart emas

O'z xatolaringiz uchun type — URI: https://vazifalar-api.example/xatolar/vazifa-topilmadi. Bu manzil identifikator — mijoz uni satr sifatida solishtiradi. RFC 9457 tavsiyasi: iloji bo'lsa, manzil shu xato haqidagi hujjatga olib borsin. Ochilmasa ham xato emas.

Bizning kanonda example domeni ishlatilgan — haqiqiy hujjat sahifasi yo'q. Xatolar ro'yxati loyiha ichidagi API.md faylida yoziladi (API dizayn konvensiyalari darsida).

Tekshirib ko'ring: Ikki javob keldi: 1) title: "Vazifa topilmadi", detail: "Vazifa 7 topilmadi"; 2) title: "Vazifa topilmadi", detail: "Vazifa 12 topilmadi". Mijoz ularni bitta toifaga qo'shishi uchun qaysi maydonga qarashi kerak?

Javob

type ga. title ham bir xil, lekin u odam uchun matn: ertaga kimdir uni "Bunday vazifa yo'q" deb tahrirlasa, mijoz kodi buziladi. type (yoki undan yasalgan kod) — mashina uchun shartnoma, u o'zgarmaydi. detail esa har holatda boshqacha — toifalash uchun yaramaydi.

3. Kengaytmalar va validatsiya xatolari

3.1 O'z maydonlaringizni qo'shish

RFC 9457 beshta maydondan tashqari kengaytma maydonlar (extension members) qo'shishga ruxsat beradi. Muhim qoida: mijoz o'zi tanimagan kengaytmani e'tiborsiz qoldirishi kerak. Shuning uchun yangi kengaytma qo'shish eski mijozlarni buzmaydi.

vazifalar-api uchta kengaytma qo'shadi:

Kengaytma Nima uchun
kod type ning qisqa varianti: "vazifa-topilmadi" — switch da qulay
xatolar validatsiya xatolari ro'yxati
sorovId X-Request-Id bilan bir xil — logdan shu so'rovni topish uchun

sorovId ni Express + TypeScript darsida X-Request-Id sarlavhasi sifatida qo'shgandik. Endi u xato tanasida ham bor. Foydalanuvchi "menda xato chiqdi" deb yozsa, qo'llab-quvvatlash xizmati shu ID bilan server logidan aynan o'sha so'rovni topadi.

3.2 Bir nechta maydon xato bo'lsa

Forma yuborildi: matn bo'sh, bajarildi esa "ha" satri. Birinchi xatoda to'xtab, faqat "matn bo'sh" deyish — yomon tajriba. Foydalanuvchi tuzatadi, yana yuboradi va yana xato oladi. Yaxshisi — hamma xatoni bitta javobda qaytarish.

RFC 9457 ning o'zida shunday misol bor: errors massivi, har elementda detail va pointer (JSON Pointer, masalan "#/age"). Biz o'zbekcha nomlar tanladik: xatolar: [{ joy, xabar }]. joy — xato qayerda: body.matn, query.limit, params.id.

json
{
  "type": "https://vazifalar-api.example/xatolar/validatsiya",
  "title": "So'rov ma'lumoti noto'g'ri",
  "status": 422,
  "detail": "matn 1–200 belgi bo'lishi kerak",
  "instance": "/vazifalar",
  "kod": "validatsiya",
  "xatolar": [
    { "joy": "body", "xabar": "matn 1–200 belgi bo'lishi kerak" }
  ]
}

Bu holat 24 dagi haqiqiy javob (sorovId siz). Hozir joy faqat "body" — qo'lda tekshiruv birinchi xatoda to'xtaydi. Zod darsida bir javobda uchta maydon xatosi va aniq body.matn yo'llari chiqadi.

Frontend bu massivdan foydalanib, har xabarni o'sha maydon ostida ko'rsatadi — RHF va Zod darsida formaga xato qo'yishni ko'rgansiz.

Maslahat: Maydon nomini tanlashda bitta qoidaga yopishing: kengaytmalar ham boshqa maydonlar kabi camelCase. Bir loyihada sorovId, boshqasida request_id — mijoz uchun yangi bosh og'riq.

4. Xato kodlari katalogi

4.1 Katalog nima?

Xato katalogi (error catalog) — API qaytara oladigan barcha domen xatolari ro'yxati: kod, status va qachon chiqishi. Bu jadval API hujjatining bir qismi. Mijoz dasturchisi uni o'qib, qaysi xatoga qanday javob berishni oldindan yozadi.

Holat 24 dagi katalog:

kod status Qachon
validatsiya 422 tana qoidaga mos emas
parametr 400 query yoki yo'l parametri noto'g'ri
vazifa-topilmadi 404 bunday ID yo'q
royxat-toldi 409 1000 ta vazifa bor
idempotentlik-* 400/409/422 keyingi darsda

type URI shu koddan yasaladi: asos + kod. Shu sabab kod va type hech qachon bir-biriga zid bo'lmaydi.

4.2 Kod — shartnoma, matn — yo'q

Katalog bilan bitta qat'iy qoida keladi: kod o'zgarmaydi, title va detail esa o'zgarishi mumkin. Ertaga detail ni tarjima qilasiz yoki aniqroq yozasiz — mijozlar buzilmaydi. Lekin vazifa-topilmadi ni topilmadi ga almashtirsangiz, bu buzuvchi o'zgarish (breaking change) — versiyalash darsida ko'ramiz.

Shuning uchun mijoz kodi matnni emas, kodni tekshiradi:

js
function explainError(problem) {
  switch (problem.kod) {
    case "vazifa-topilmadi":
      return "Vazifa allaqachon o'chirilgan";
    case "royxat-toldi":
      return "Ro'yxat to'lgan — eskilarini o'chiring";
    default:
      return problem.detail ?? problem.title;
  }
}

console.log(explainError({ kod: "royxat-toldi", status: 409 }));
console.log(explainError({ title: "Not Found", status: 404 }));
console.log(explainError({
  title: "Bad Request",
  detail: "Tana yaroqli JSON emas",
}));

Konsolda:

text
Ro'yxat to'lgan — eskilarini o'chiring
Not Found
Tana yaroqli JSON emas

default tarmog'i muhim: server yangi kod qo'shsa, eski mijoz baribir detail yoki title ni ko'rsatadi.

Tekshirib ko'ring: Mijoz if (problem.title === "Vazifa topilmadi") deb yozgan. Server dasturchisi sarlavhani "Bunday vazifa yo'q" ga o'zgartirdi. Nima bo'ladi va kim aybdor?

Javob

Mijozdagi shart endi hech qachon bajarilmaydi — xato boshqa tarmoqqa tushadi. Server dasturchisi qoidani buzmadi: title — odam uchun matn. Mijoz type yoki kod ni tekshirishi kerak edi. Katalog hujjatida shu qoidani ochiq yozish — ikkala tomonni himoya qiladi.

5. Problem Details'ni yasash va o'qish

5.1 Yasovchi funksiya

Har xato ishlovchisi Problem obyektini qo'lda yig'masin — bitta toza funksiya kerak. Quyidagi blok brauzerda ham ishlaydi («Ishga tushir»):

js
const BASE = "https://vazifalar-api.example/xatolar/";
const STATUS_TEXT = {
  400: "Bad Request", 404: "Not Found",
  409: "Conflict", 422: "Unprocessable Content",
  500: "Internal Server Error",
};

function makeProblem(status, { code, title, ...rest } = {}) {
  const p = {
    type: code ? BASE + code : "about:blank",
    title: code ? title : (STATUS_TEXT[status] ?? "Error"),
    status,
    ...rest,
  };
  if (code) p.code = code;
  return p;
}

console.log(makeProblem(404, {
  detail: "GET /yoq — bunday manzil yo'q",
}));
console.log(makeProblem(409, {
  code: "royxat-toldi",
  title: "Ro'yxat to'lgan",
  detail: "1000 tadan ortiq vazifa saqlanmaydi",
}).type);

Konsolda:

text
{
  type: 'about:blank',
  title: 'Not Found',
  status: 404,
  detail: "GET /yoq — bunday manzil yo'q"
}
https://vazifalar-api.example/xatolar/royxat-toldi

Ikki qoidaga qarang. code bo'lmasa — about:blank va standart status matni. code bo'lsa — katalog URI va o'z sarlavhangiz. Serverda status matnlari jadvali tayyor bor: http.STATUS_CODES (createServer darsi). Brauzerda u yo'q, shuning uchun kichik jadval yozdik.

Diqqat: 422 ning standart nomi RFC 9110 da "Unprocessable Content". Node'ning STATUS_CODES[422] esa eski nomni beradi: "Unprocessable Entity" — kanondagi curl chiqishida ham shunday. Ma'nosi bir xil.

5.2 Mijoz tomonda o'qish

Mijoz javobni qanday tanib oladi? Content-Type ga qaraydi. Soxta Response bilan sinaymiz (bu ham brauzer bloki):

js
async function readError(res) {
  const type = res.headers.get("content-type") ?? "";
  if (type.startsWith("application/problem+json")) {
    const p = await res.json();
    const kind = p.kod ?? "about:blank";
    return `${p.status} ${kind}: ${p.detail ?? p.title}`;
  }
  return `${res.status}: ${(await res.text()).slice(0, 40)}`;
}

const problemRes = new Response(JSON.stringify({
  type: "https://vazifalar-api.example/xatolar/parametr",
  title: "Query parametr noto'g'ri", status: 400,
  detail: "limit — 1..100 oralig'idagi butun son", kod: "parametr",
}), {
  status: 400,
  headers: {
    "content-type": "application/problem+json; charset=utf-8",
  },
});
const htmlRes = new Response("<h1>502 Bad Gateway</h1>", {
  status: 502,
});

console.log(await readError(problemRes));
console.log(await readError(htmlRes));

Konsolda:

text
400 parametr: limit — 1..100 oralig'idagi butun son
502: <h1>502 Bad Gateway</h1>

Ikkinchi holat ataylab: API oldidagi proxy (nginx) server o'chganda o'zining HTML sahifasini qaytaradi. Mijoz har doim "Problem emas" holatiga tayyor bo'lsin. startsWith ham bejiz emas: sarlavhada ; charset=utf-8 qo'shimchasi bo'lishi mumkin.

6. Serverda: hamma xato bitta yo'ldan

6.1 Uch toifa xato

Server xatolarini uch toifaga ajratamiz. Diagrammada har biri qaysi Problem'ga aylanishini kuzating:

flowchart TD
  X[Xato tashlandi] --> A{Qanday xato?}
  A -->|HttpXato: o'zimizniki| B[katalog type + kod]
  A -->|http-errors: body-parser| C[about:blank + status]
  A -->|boshqa: kutilmagan| D[500, detail yashirin]
  D --> L[log: sorovId + stack]
  B --> R[application/problem+json]
  C --> R
  D --> R
  1. O'zimiz tashlagan xato (HttpXato) — status, kod, sarlavha bizda bor.
  2. Kutubxona xatosi — express.json() buzuq JSON'da status: 400, expose: true maydonli xato tashlaydi (middleware darsi). Bu umumiy HTTP xatosi → about:blank.
  3. Kutilmagan xato — kod xatosi, DB uzilishi. Mijozga faqat "kutilmagan xato" va sorovId; asl xabar va stack — faqat logga.

6.2 Node'da ishlaydigan namuna

Quyidagi blok kichik node:http serverni ochadi va unga uchta so'rov yuboradi. Uchinchi so'rovda ichki xato "parol" so'zini o'z ichiga oladi. Blok Node uchun — o'z terminalingizda ishga tushiring:

js
import { createServer, STATUS_CODES } from "node:http";

const BASE = "https://vazifalar-api.example/xatolar/";
let requestNo = 0;

function problem(status, { code, title, ...rest } = {}) {
  return {
    type: code ? BASE + code : "about:blank",
    title: code ? title : STATUS_CODES[status],
    status,
    ...rest,
  };
}

function send(res, p) {
  res.writeHead(p.status, {
    "content-type": "application/problem+json",
  });
  res.end(JSON.stringify(p));
}

const server = createServer((req, res) => {
  const requestId = `s-${++requestNo}`;
  const common = { instance: req.url, requestId };
  try {
    if (req.url === "/tasks/7") {
      return send(res, problem(404, {
        code: "vazifa-topilmadi", title: "Vazifa topilmadi",
        detail: "Vazifa 7 topilmadi", ...common,
      }));
    }
    if (req.url === "/boom") {
      throw new Error("DB parol=maxfiy bilan ulanmadi");
    }
    const detail = "Bunday manzil yo'q";
    send(res, problem(404, { detail, ...common }));
  } catch (error) {
    console.log(`[log ${requestId}] ${error.message}`);
    send(res, problem(500, {
      detail: "Kutilmagan xato. requestId bilan murojaat qiling.",
      ...common,
    }));
  }
});
await new Promise((ok) => server.listen(0, "127.0.0.1", ok));
const base = `http://127.0.0.1:${server.address().port}`;

for (const path of ["/tasks/7", "/yoq", "/boom"]) {
  const res = await fetch(base + path);
  const p = await res.json();
  console.log(res.status, res.headers.get("content-type"));
  console.log(`  ${p.type} | ${p.title} | ${p.detail}`);
}
server.closeAllConnections();
server.close();

Konsolda:

text
404 application/problem+json
  https://vazifalar-api.example/xatolar/vazifa-topilmadi | Vazifa topilmadi | Vazifa 7 topilmadi
404 application/problem+json
  about:blank | Not Found | Bunday manzil yo'q
[log s-3] DB parol=maxfiy bilan ulanmadi
500 application/problem+json
  about:blank | Internal Server Error | Kutilmagan xato. requestId bilan murojaat qiling.

Uchinchi so'rovda "parol=maxfiy" faqat [log …] qatorida — server logida. Mijoz uni ko'rmadi. Lekin s-3 ikkalasida ham bor: shu ID bilan log va javobni bog'lash mumkin.

Tekshirib ko'ring: Nega 500 da title "Internal Server Error", lekin detail ichki xabar emas?

Javob

500 — kutilmagan xato, uning domen turi yo'q, shuning uchun about:blank va standart title. detail ga asl xabarni qo'ysak, u parol, SQL so'rovi, fayl yo'li yoki kutubxona versiyasini oshkor qilishi mumkin. Mijozga faqat "nima qilish kerak" (sorovId bilan murojaat qilish) yetadi; tafsilot dasturchiga logdan kerak.

7. Hujumchi nigohi

Xato javobi — hujumchi uchun ma'lumot konida. U ataylab xato so'rovlar yuboradi va javoblarni o'qiydi:

Hujum Xato javobida nima oqadi Bizning himoya
Buzuq JSON yuborish kutubxona nomi, stack, fayl yo'llari (C:/Users/…/node_modules/…) 400 about:blank, o'z matnimiz "Tana yaroqli JSON emas"
DB'ni yiqitishga urinish SQL matni, jadval nomlari, parol satri 500 da detail yashirin, asl xabar faqat logda
ID'larni sanab chiqish "foydalanuvchi bor, lekin ruxsat yo'q" vs "yo'q" auth darslarida (28-qism): ikkalasiga bir xil 404

Kanondagi test aynan shuni tekshiradi: soxta ombor "parol=maxfiy" xabari bilan xato tashlaydi. Test javob tanasida bu so'z yo'qligini, logda esa sorovId bilan borligini tasdiqlaydi.

Yana bir nozik joy: detail ga foydalanuvchi kiritgan qiymatni qo'shsangiz (masalan "'<script>…' noto'g'ri"), frontend uni HTML sifatida chiqarmasligi kerak. React matnni o'zi escape qiladi, lekin innerHTML bilan chiqargan kod — XSS (XSS darsi). Kanonda kiritilgan qiymat qisqartiriladi: x.slice(0, 40).

8. API qadami: Problem Details (holat 24, 1-qism)

Bu qadam vazifalar-api ning holat 22 dan boshlanadi. Commit — keyingi darsda, Idempotency-Key bilan birga (holat 24). Hozircha commit qilmang.

8.1 src/umumiy/problem.ts

Yangi fayl — to'liq (kanondan; uzun qatorlar telefon uchun bo'lingan, ma'no aynan):

ts
// problem.ts — Problem Details (RFC 9457, RFC 7807 o'rnini oldi):
// hamma xato javobi bir xil, mashina o'qiy oladigan shaklda
import { STATUS_CODES } from "node:http";
import type { Response } from "express";
import type { MaydonXatosi } from "./http-xato.ts";

/** Xato turlari katalogi: type URI → tavsif.
 *  Yangi kod qo'shilsa — shu yerga (API.md da ham) */
export const XATO_TURI_ASOSI =
  "https://vazifalar-api.example/xatolar/";

export type Problem = {
  /** URI — xato turi; umumiy HTTP xatolari uchun "about:blank"
   *  (title = status matni) */
  type: string;
  title: string;
  status: number;
  detail?: string;
  /** shu holatning manzili — qaysi so'rovda yuz berdi */
  instance?: string;
  /** kengaytmalar (RFC 9457 §3.2): mashina o'qiydigan kod,
   *  validatsiya ro'yxati, so'rov ID */
  kod?: string;
  xatolar?: MaydonXatosi[];
  sorovId?: string;
};

export function problemYasa(holat: number, sozlama: {
  kod?: string | undefined;
  sarlavha?: string | undefined;
  detail?: string;
  instance?: string;
  xatolar?: MaydonXatosi[] | undefined;
  sorovId?: string;
}): Problem {
  const umumiy = STATUS_CODES[holat] ?? "Xato";
  const p: Problem = {
    type: sozlama.kod === undefined
      ? "about:blank" : XATO_TURI_ASOSI + sozlama.kod,
    title: sozlama.kod === undefined
      ? umumiy : (sozlama.sarlavha ?? umumiy),
    status: holat,
  };
  if (sozlama.detail !== undefined) p.detail = sozlama.detail;
  if (sozlama.instance !== undefined) p.instance = sozlama.instance;
  if (sozlama.kod !== undefined) p.kod = sozlama.kod;
  if (sozlama.xatolar !== undefined) p.xatolar = sozlama.xatolar;
  if (sozlama.sorovId !== undefined) p.sorovId = sozlama.sorovId;
  return p;
}

export function problemYubor(res: Response, p: Problem): void {
  // res.json() Content-Type ni application/json qiladi —
  // avval o'zimiz qo'yamiz, json() uni o'zgartirmaydi
  res.status(p.status).type("application/problem+json").json(p);
}

problemYasa — «Yasovchi funksiya» bo'limidagi makeProblem ning o'zi, faqat STATUS_CODES bilan. Har ixtiyoriy maydon if bilan qo'shiladi: tsconfig da exactOptionalPropertyTypes yoqilgan, detail: undefined yozib bo'lmaydi.

problemYubor dagi izohga e'tibor bering. res.json() faqat Content-Type hali qo'yilmagan bo'lsa application/json qo'yadi. Shuning uchun tartib muhim: avval .type(...), keyin .json(...).

8.2 src/umumiy/http-xato.ts — to'liq

Klassga kod, sarlavha, xatolar ro'yxati va qo'shimcha HTTP sarlavhalar qo'shildi:

ts
// http-xato.ts — status kodli xato; xato ishlovchi uni
// Problem Details (RFC 9457) ga aylantiradi
// (#32 da iyerarxiya bo'ladi)

/** Validatsiya xatosi tafsiloti: qaysi joy (body.matn, query.limit)
 *  va nima noto'g'ri */
export type MaydonXatosi = { joy: string; xabar: string };

export type XatoSozlamasi = ErrorOptions & {
  /** mashina o'qiydigan qisqa kod — katalogdagi type URI shundan */
  kod?: string;
  /** odam o'qiydigan qisqa sarlavha (har kod uchun bir xil) */
  sarlavha?: string;
  xatolar?: MaydonXatosi[];
  /** javobga qo'shimcha HTTP sarlavhalar (Retry-After, Allow …) */
  httpSarlavhalar?: Record<string, string>;
};

export class HttpXato extends Error {
  readonly holat: number;
  readonly kod: string | undefined;
  readonly sarlavha: string | undefined;
  readonly xatolar: MaydonXatosi[] | undefined;
  readonly httpSarlavhalar: Record<string, string>;

  /** @param xabar Problem Details `detail` — aynan shu holat haqida */
  constructor(holat: number, xabar: string,
    sozlama: XatoSozlamasi = {}) {
    super(xabar, sozlama.cause === undefined
      ? undefined : { cause: sozlama.cause });
    this.name = "HttpXato";
    this.holat = holat;
    this.kod = sozlama.kod;
    this.sarlavha = sozlama.sarlavha;
    this.xatolar = sozlama.xatolar;
    this.httpSarlavhalar = sozlama.httpSarlavhalar ?? {};
  }
}

/** 422: so'rov sintaktik to'g'ri (JSON), lekin qiymatlar qoidaga
 *  mos emas */
export function validatsiyaXatosi(joy: string, xabar: string): HttpXato {
  return new HttpXato(422, xabar, {
    kod: "validatsiya",
    sarlavha: "So'rov ma'lumoti noto'g'ri",
    xatolar: [{ joy, xabar }],
  });
}

ErrorOptions — standart { cause } turi: ichki xato zanjiri saqlanadi (error.cause darsi). Xizmat va marshrutlardagi eski new HttpXato(404, "…") chaqiruvlariga endi kod va sarlavha qo'shiladi: masalan { kod: "vazifa-topilmadi", sarlavha: "Vazifa topilmadi" }.

8.3 Xato ishlovchisi (src/ilova.ts)

ilova.ts dagi xato ishlovchisi va uning yordamchilari — to'liq; 404 middleware ham endi HttpXato tashlaydi:

ts
import { HttpXato } from "./umumiy/http-xato.ts";
import { problemYasa, problemYubor } from "./umumiy/problem.ts";

function httpErrorsmi(x: unknown):
  x is Error & { status: number; expose: boolean; type?: unknown } {
  return x instanceof Error
    && "status" in x && typeof x.status === "number"
    && "expose" in x && typeof x.expose === "boolean";
}

// body-parser xatolari (`type` maydoni) → bizning kodlar
const TANA_XATOLARI: Record<string, string> = {
  "entity.parse.failed": "Tana yaroqli JSON emas",
  "entity.too.large": "Tana 16 KB dan katta",
  "encoding.unsupported": "Kodlash qo'llab-quvvatlanmaydi",
};

const xatoIshlovchi: ErrorRequestHandler =
  (xato: unknown, req, res, next) => {
    if (res.headersSent) {
      next(xato); // javob yarmida — Express o'zi ulanishni uzadi
      return;
    }
    const umumiy = { instance: req.originalUrl, sorovId: req.sorovId };
    if (xato instanceof HttpXato) {
      res.set(xato.httpSarlavhalar);
      problemYubor(res, problemYasa(xato.holat, {
        ...umumiy, kod: xato.kod, sarlavha: xato.sarlavha,
        detail: xato.message, xatolar: xato.xatolar,
      }));
      return;
    }
    // Boshqa kutubxonalar (body-parser 400/413, serve-static 416…)
    // http-errors tashlaydi: status + expose
    if (httpErrorsmi(xato) && xato.status < 500 && xato.expose) {
      const tur = typeof xato.type === "string" ? xato.type : "";
      problemYubor(res, problemYasa(xato.status, {
        ...umumiy, detail: TANA_XATOLARI[tur] ?? xato.message,
      }));
      return;
    }
    // Kutilmagan: tafsilot (stack, xabar) mijozga BERILMAYDI
    console.error(`[${req.sorovId}]`, xato);
    problemYubor(res, problemYasa(500, {
      ...umumiy,
      detail: "Kutilmagan xato. sorovId bilan murojaat qiling.",
    }));
  };

// ilovaYarat(...) ichida, routerlardan keyin:
app.use((req) => {
  throw new HttpXato(404, `${req.method} ${req.path} — bunday manzil yo'q`);
});
app.use(xatoIshlovchi);

TANA_XATOLARI — body-parser'ning type maydonini o'zbekcha matnga aylantiradi. 404 middleware endi HttpXato tashlaydi — Express'ning HTML sahifasi qolmadi.

Buzuq JSON bilan kelgan POST /vazifalar zanjir bo'ylab qanday yurishini kuzating:

Qadam Middleware Natija
1 sorovId() req.sorovId va X-Request-Id sarlavhasi qo'yildi
2 helmet(), cors() xavfsizlik va CORS sarlavhalari
3 express.json() {matn o'qilmadi — status: 400, type: "entity.parse.failed" xatosi tashlandi
4 routerlar, 404 middleware o'tkazib yuboriladi — Express to'g'ri xato ishlovchiga sakraydi
5 xatoIshlovchi httpErrorsmi → about:blank, detail: "Tana yaroqli JSON emas"

To'rtinchi qatorga e'tibor bering: xato tashlangandan keyin oddiy middleware'lar ishlamaydi (Middleware va request lifecycle). Shuning uchun sarlavhalarni qo'yadigan middleware'lar (1–2) erta turadi — xato javobida ham X-Request-Id va CORS sarlavhalari bo'ladi.

8.4 Natija (kanon curl, sorovId olib tashlangan)

bash
curl $A/vazifalar/99
curl "$A/vazifalar?limit=500"
curl -X POST $A/vazifalar \
  -H 'Content-Type: application/json' -d '{matn'
curl $A/yoq
text
{"type":"https://vazifalar-api.example/xatolar/vazifa-topilmadi","title":"Vazifa topilmadi","status":404,"detail":"Vazifa 99 topilmadi","instance":"/vazifalar/99","kod":"vazifa-topilmadi"}
{"type":"https://vazifalar-api.example/xatolar/parametr","title":"Query parametr noto'g'ri","status":400,"detail":"limit — 1..100 oralig'idagi butun son","instance":"/vazifalar?limit=500","kod":"parametr"}
{"type":"about:blank","title":"Bad Request","status":400,"detail":"Tana yaroqli JSON emas","instance":"/vazifalar"}
{"type":"about:blank","title":"Not Found","status":404,"detail":"GET /yoq — bunday manzil yo'q","instance":"/yoq"}

instance — so'rov yo'li query bilan birga (/vazifalar?limit=500). Kanon o'lchovi: 404 javob 5 893 req/s, oddiy GET /vazifalar/5 esa 5 874 req/s (autocannon, band mashina, bitta o'lchov). Problem yasash narxi sezilmaydi.

8.5 Bu buzuvchi o'zgarish

Eski { "xato": "…" } endi yo'q. 22-qismdagi frontend'ning serverXabari funksiyasi aynan xato maydonini o'qiydi — u endi xabarni topmaydi va "Server xatosi: 422" ko'rsatadi. Bu haqiqiy buzilish: versiyalash darsida API'ni /v1 ga ko'chirish sababi aynan shu. Frontend'ni API'ga ulaganda (Coverage va test sifati metrikalari darsidagi «Frontend'ni ulash» bo'limi) serverXabari detail ni o'qiydigan bo'ladi (eski xato ham o'qiladi).

9. Ko'p uchraydigan xatolar

1. Xatoni 200 bilan qaytarish. res.json({ success: false, error: "…" }) — status 200. Mijoz res.ok ni tekshiradi va xatoni muvaffaqiyat deb oladi; keshlar va monitoring ham "hammasi yaxshi" deb hisoblaydi. To'g'risi: mos status + Problem.

2. Tanadagi status javob statusidan farq qiladi. Javob 400, tanada "status": 422. RFC 9457: status — faqat maslahat, haqiqat HTTP statusi. Ikkalasini bitta joydan oling: res.status(p.status).

3. Content-Type: application/json qolib ketdi. res.type(...) ni res.json() dan keyin chaqirsangiz — kech, sarlavha yuborilgan. Mijoz problem+json ni kutsa, tanimaydi.

4. title ga holat tafsilotini yozish. title: "Vazifa 99 topilmadi" — endi har ID uchun boshqa sarlavha, toifalash buziladi. Tafsilot — detail da.

5. 500 da error.message ni qaytarish. Qulay tuyuladi, lekin Prisma yoki pg xatosi SQL, jadval va ustun nomlarini ichida olib yuradi. Faqat sorovId qaytaring.

6. Mijoz === "application/json" bilan tekshiradi. application/problem+json; charset=utf-8 mos kelmaydi va xato tanasi o'qilmaydi. startsWith yoki includes("json") bilan tekshiring.

10. Mashqlar

Yechimlar kurs/mashqlar/25/23-problem-details/ da.

1-mashq (oson): to'ldiring

  • Problem Details javobining media turi — application/
  • Umumiy HTTP xatosi uchun type qiymati —
  • Aynan shu holatga xos tushuntirish maydoni —
  • Amaldagi standart raqami — RFC
  • Mijoz tanimagan kengaytma maydonini nima qilishi kerak?
Yechim
  • problem+json — oddiy application/json emas.
  • about:blank — title shunda status kodining standart matni.
  • detail — title esa tur uchun bir xil.
  • 9457 — 2023; RFC 7807 o'rnini oldi.
  • e'tiborsiz qoldiradi — shuning uchun yangi kengaytma eski mijozni buzmaydi.

2-mashq (o'rta): validatsiya xatosi

validationProblem(instance, errors) funksiyasini yozing. errors — [{ joy, xabar }] massivi. Natija: status 422, kod: "validatsiya", katalog type, title: "So'rov ma'lumoti noto'g'ri", detail — bitta xato bo'lsa uning xabari, ko'p bo'lsa "N ta maydon noto'g'ri". errors bo'sh bo'lsa — Error tashlasin (bo'sh validatsiya xatosi — dasturchi xatosi). Bu toza JS — «Ishga tushir» bilan ishlaydi.

Yechim
js
const BASE = "https://vazifalar-api.example/xatolar/";

function validationProblem(instance, errors) {
  if (errors.length === 0) {
    throw new Error("validationProblem: xatolar bo'sh");
  }
  return {
    type: BASE + "validatsiya",
    title: "So'rov ma'lumoti noto'g'ri",
    status: 422,
    detail: errors.length === 1
      ? errors[0].xabar
      : `${errors.length} ta maydon noto'g'ri`,
    instance,
    kod: "validatsiya",
    xatolar: errors,
  };
}

const one = validationProblem("/vazifalar", [
  { joy: "body.matn", xabar: "matn 1–200 belgi bo'lishi kerak" },
]);
const many = validationProblem("/vazifalar", [
  { joy: "body.matn", xabar: "matn bo'sh" },
  { joy: "body.bajarildi", xabar: "true yoki false" },
]);
console.log(one.detail);
console.log(many.detail, many.xatolar.map((x) => x.joy).join(", "));
try {
  validationProblem("/vazifalar", []);
} catch (error) {
  console.log(error.message);
}

Konsolda:

text
matn 1–200 belgi bo'lishi kerak
2 ta maydon noto'g'ri body.matn, body.bajarildi
validationProblem: xatolar bo'sh

3-mashq (qiyin): mijoz uchun ApiError

fetchJson(url, init) yozing: res.ok bo'lsa JSON qaytarsin. Aks holda ApiError tashlasin: status, code (Problem'dagi kod, yo'q bo'lsa type ning oxirgi bo'lagi, about:blank bo'lsa null), message (detail → title → "HTTP <status>"), fields (xatolar yoki []). Problem bo'lmagan javobda ham ishlasin. Soxta fetch bilan uch holatni sinang.

Yechim
js
class ApiError extends Error {
  constructor(status, code, message, fields) {
    super(message);
    this.name = "ApiError";
    this.status = status;
    this.code = code;
    this.fields = fields;
  }
}

async function fetchJson(url, init, fetchFn = fetch) {
  const res = await fetchFn(url, init);
  if (res.ok) return res.json();
  const type = res.headers.get("content-type") ?? "";
  if (!type.startsWith("application/problem+json")) {
    throw new ApiError(res.status, null, `HTTP ${res.status}`, []);
  }
  const p = await res.json();
  const fromType = p.type && p.type !== "about:blank"
    ? p.type.split("/").at(-1)
    : null;
  throw new ApiError(
    res.status,
    p.kod ?? fromType,
    p.detail ?? p.title ?? `HTTP ${res.status}`,
    p.xatolar ?? [],
  );
}

const PJ = { "content-type": "application/problem+json" };
const answers = {
  "/a": () => new Response('{"id":1}', { status: 200 }),
  "/b": () => new Response(JSON.stringify({
    type: "https://vazifalar-api.example/xatolar/validatsiya",
    title: "So'rov ma'lumoti noto'g'ri", status: 422,
    xatolar: [{ joy: "body.matn", xabar: "bo'sh" }],
  }), { status: 422, headers: PJ }),
  "/c": () => new Response("Bad Gateway", { status: 502 }),
};
const fakeFetch = async (url) => answers[url]();

for (const url of ["/a", "/b", "/c"]) {
  try {
    console.log(await fetchJson(url, {}, fakeFetch));
  } catch (e) {
    console.log(e.name, e.status, e.code, e.message, e.fields.length);
  }
}

Konsolda:

text
{ id: 1 }
ApiError 422 validatsiya So'rov ma'lumoti noto'g'ri 1
ApiError 502 null HTTP 502 0

/b da kod yo'q — kod type ning oxirgi bo'lagidan olindi. /c — proxy'ning oddiy matni: Problem emas, lekin mijoz baribir bir xil ApiError oladi va UI kodi bitta tarmoq bilan ishlaydi.

4-mashq: API qadami

O'z vazifalar-api ingizda «API qadami» bo'limidagi uch o'zgarishni qiling: problem.ts, kengaygan HttpXato + validatsiyaXatosi, yangi xato ishlovchi (404 middleware ham HttpXato tashlasin). Eski { xato } javoblari qolmasin: grep -rn "xato:" src faqat tur va o'zgaruvchilarni ko'rsatsin. curl bilan «Natija» bo'limidagi to'rt so'rovni yuboring. Bitta test qo'shing: 422 javobining content-type — application/problem+json; charset=utf-8, tanasi deepEqual bilan kutilgan obyektga teng (sorovId dan tashqari). Commit qilmang — keyingi darsda Idempotency-Key bilan birga.

11. Real ishda

  • Spring Boot 3 va ASP.NET Core Problem Details'ni sukut bo'yicha qaytaradi (ProblemDetail, ProblemDetails klasslari). Java yoki C# jamoasining API'si bilan ishlasangiz, shu formatni ko'rasiz.
  • NestJS da (30-qism) global exception filter shu shaklni yasaydi; OpenAPI hujjatida (OpenAPI 3.1 darsi) xato javoblari bitta Problem sxemasi bilan tasvirlanadi.
  • Stripe, GitHub kabi katta API'lar o'z formatini RFC'dan oldin yaratgan: Stripe — { error: { type, code, message, param } }. G'oya bir xil: barqaror code va odam uchun message. Yangi API'da o'z formatini o'ylab topishga sabab yo'q.
  • Intervyuda: "API xatolarini qanday qaytarasiz?" — status kod + RFC 9457 + barqaror kod + validatsiya ro'yxati + 500 da tafsilot yashirish. Bu javob tajribani ko'rsatadi.

Xulosa

  • Har xato bir xil shaklda: application/problem+json, type, title, status, detail, instance (RFC 9457, 2023).
  • type — mashina uchun barqaror identifikator; title — tur uchun; detail — shu holat uchun.
  • Umumiy HTTP xatosi — about:blank va standart status matni; domen xatosi — katalog URI va kod.
  • Validatsiya xatolari bitta javobda, massivda: xatolar: [{ joy, xabar }].
  • 500 da ichki xabar faqat logga; javobda — sorovId.
  • Xato formatini o'zgartirish — buzuvchi o'zgarish; mijozlar oldindan ogohlantiriladi.

Keyingi dars: Idempotency-Key — tarmoq uzilib mijoz POST'ni qayta yuborsa, ikkinchi vazifa (yoki ikkinchi to'lov) yaratilmasligini ta'minlaymiz.

Manbalar

  • RFC 9457 "Problem Details for HTTP APIs" (2023-07) — rfc-editor.org/rfc/rfc9457
  • RFC 9110 "HTTP Semantics", §15 status kodlari (2022-06) — rfc-editor.org/rfc/rfc9110
  • IANA "HTTP Problem Types" registry — iana.org/assignments/http-problem-types
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
API xato javoblari: Problem Details (RFC 9457) — type, title, status, detail — IlmHamroh