IlmHamroh
JavaScript Full-stack/25-qism. HTTP, Express va REST API33/51-dars23 daqiqa
Mundarija (34)

Express global error handler: 4 argumentli middleware va 404 ishlovchi

Qisqacha: Express xato ishlovchini argumentlar soni bo'yicha taniydi: (err, req, res, next) — to'rtta. Uchta yozsangiz, u oddiy middleware bo'lib qoladi va xatoni hech qachon ko'rmaydi. Ishlovchi barcha marshrutlardan va 404 ishlovchidan keyin turadi. Uning vazifasi: o'z xatolarimizni Problem Details'ga aylantirish, kutubxona xatolarini (yaroqsiz JSON, 413) tanish, qolganiga 500 va umumiy matn berish — stack va asl xabar faqat logga. O'z ishlovchingiz bo'lmasa, Express'ning sukut ishlovchisi development'da stack'ni mijozga HTML qilib ko'rsatadi — biz buni o'lchadik.

Bu darsda

  • 4 argumentli middleware'ni yozasiz va Express uni qanday tanishini tushuntira olasiz.
  • next(err), throw (sinxron va async) va next('route') farqini bilasiz.
  • 404 ishlovchini to'g'ri joyga qo'yasiz.
  • body-parser va serve-static xatolarini (http-errors) tanib, o'zbekcha javobga aylantirasiz.
  • 500 da stack va ichki xabarni yashirib, logga sorovId bilan yozasiz; headersSent holatini to'g'ri hal qilasiz.

Oldin bilishingiz kerak: Xato turlari va custom error iyerarxiyasi, Middleware va request lifecycle, Express 5: async xatolar, Xato javoblari: Problem Details.

1. Nega bu kerak?

Oldingi darsda xatolarni turlarga ajratdik: TopilmadiXatosi, ValidatsiyaXatosi va boshqalar. Ular throw qilinadi — lekin keyin nima bo'ladi? Kimdir ularni ushlab, javobga aylantirishi kerak.

Har marshrutda try/catch yozish mumkin. 15 ta marshrut — 15 ta bir xil catch bloki. Bittasida sorovId unutiladi, boshqasida stack javobga tushib qoladi. Bunday kodni kim tekshiradi?

To'g'ri yo'l — bitta joy. Hamma xato oxir-oqibat o'sha yerga oqib keladi, u yerda bir xil qoida bilan javob yasaladi va bir xil formatda log yoziladi. Express'da bu joy — global xato ishlovchi (global error handler).

O'xshatish: kasalxonaning qabul bo'limi. Qaysi qavatda nima bo'lmasin — bemor bitta bo'limga keladi. U yerda navbatchi shifokor tashxis qo'yadi: "yengil — uyga", "og'ir — reanimatsiya". Har qavatda alohida tez yordam bo'lmaydi.

2. Ishlovchi bo'lmasa nima bo'ladi?

2.1 Biz o'lchadik: Express'ning sukut ishlovchisi

Express 5.3.0 da hech qanday xato ishlovchisiz marshrut yozdik:

js
import express from "express";
const app = express();
app.get("/", () => {
  throw new Error("maxfiy: parol=123");
});

Biz ishga tushirdik (Node 24.21.0, NODE_ENV berilmagan) — fetch javobi:

text
500 text/html; charset=utf-8 | <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Error: maxfiy: parol=123<br> &nbsp; &nbsp;at file:///C:/Users/…

Mijoz uch narsani ko'rdi: xato matni (parol=123), fayl yo'li (C:/Users/… — kompyuter foydalanuvchi nomi ham) va stack — qaysi kutubxona, qaysi versiya, qaysi qator. Hujumchi uchun bu xarita.

Xuddi shu kodni NODE_ENV=production bilan:

text
500 text/html; charset=utf-8 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Internal Server Error</pre> </body> </html>

Production'da yashirildi. Lekin uchta muammo qoldi: javob HTML (bizning API JSON va application/problem+json va'da qilgan), sorovId yo'q, va NODE_ENV ni unutish — butun himoyani o'chiradi. Ikkala holatda ham Express stack'ni server terminaliga (stderr) yozdi.

Diqqat: "Production'da yashiriladi" — himoya emas, tasodif. NODE_ENV env darsida ko'rasiz — u bir harf xato bilan (prod) jim o'chib qoladi.

3. To'rt argument: Express xato ishlovchini qanday taniydi

3.1 Qoida — fn.length

JavaScript'da har funksiyaning length xususiyati bor — u e'lon qilingan parametrlar soni. Express middleware qo'shilganda shunga qaraydi: 4 ta — xato ishlovchi, aks holda — oddiy.

js
const oddiy = (req, res, next) => {};
const xatoUchun = (err, req, res, next) => {};
const unutilgan = (err, req, res) => {};
console.log(oddiy.length, xatoUchun.length, unutilgan.length);
// 3 4 3

Konsolda:

text
3 4 3

Uchinchisiga qarang: nomi err bo'lsa ham, Express uchun u oddiy middleware. Parametr nomi ahamiyatsiz, faqat soni.

3.2 Model: zanjir xatoni qanday o'tkazadi

Express'ning qoidasini 25 qatorlik modelda ko'ramiz. Bu Express emas — uning mantiqining soddalashtirilgan nusxasi, brauzerda ishlaydi:

js
function zanjir(qatlamlar) {
  return (sorov) => {
    const javob = { holat: 200, tana: "" };
    let i = 0;
    let xato;
    function next(x) {
      if (x !== undefined) xato = x;
      const q = qatlamlar[i++];
      if (!q) return;
      const xatoUchun = q.length === 4;
      // tur mos kelmasa — o'tkazib yubor
      if (Boolean(xato) !== xatoUchun) return next();
      try {
        if (xato) q(xato, sorov, javob, next);
        else q(sorov, javob, next);
      } catch (e) {
        next(e);
      }
    }
    next();
    return javob;
  };
}

const ilova = zanjir([
  (req, res, next) => { console.log("1. log"); next(); },
  (req, res, next) => { throw new Error("buzildi"); },
  (req, res, next) => { console.log("3. bu ishlamaydi"); },
  (err, req, res, next) => {
    console.log("4. ushlandi:", err.message);
    res.holat = 500;
  },
]);
console.log(ilova({}));

Konsolda:

text
1. log
4. ushlandi: buzildi
{ holat: 500, tana: '' }

Qoida bitta qatorda: xato bo'lsa — faqat 4 argumentlilar, bo'lmasa — faqat 3 argumentlilar. Uchinchi qatlam o'tkazib yuborildi, chunki zanjirda xato bor edi.

3.3 Haqiqiy Express'da 3 argumentli "ishlovchi"

Endi haqiqiy Express 5.3.0 da tekshirdik (biz ishga tushirdik, t33):

js
app.get("/", () => { throw new Error("buzildi"); });
app.use((err, req, res) => {        // ❌ 3 ta argument
  res.status(500).json({ ushlandi: err.message });
});
text
1. (err, req, res) — 3 argument: 500 text/html; charset=utf-8 | <!DOCTYPE html> … <pre>Error: buzildi<br> &nbsp; &nbsp;at file:///C:/Users/…

Bizning "ishlovchi" chaqirilmadi — javob Express'ning HTML'i, stack bilan. Yana yomoni: xatosiz so'rovda u oddiy middleware bo'lib chaqiriladi. err o'rniga so'rov, res o'rniga next funksiyasi keladi:

text
   chaqirildi! err = IncomingMessage | res = function

Bu yerda res.status(...) — next.status(...), ya'ni TypeError. TypeScript bunday holatda yordam beradi: kanon ErrorRequestHandler turi bilan yozadi va next ni ishlatmasa ham ro'yxatda qoldiradi.

Tekshirib ko'ring: ESLint no-unused-vars qoidasi "next ishlatilmagan" deb ogohlantirdi. Uni o'chirib tashlasangiz nima bo'ladi?

Javob

Funksiya 3 argumentli bo'lib qoladi va Express uni oddiy middleware deb hisoblaydi — xato ishlovchi butunlay ishlamay qoladi. To'g'ri yo'l: next ni qoldirish va ogohlantirishni o'chirish (_next nomi bilan yoki ESLint izohi bilan) yoki next ni haqiqatan ishlatish (kanondagi headersSent tarmog'i aynan next(xato) chaqiradi).

4. Xato zanjirga qanday tushadi

4.1 To'rt yo'l

Kod Ishlovchiga yetadimi?
Sinxron handler'da throw Ha — Express try/catch ichida chaqiradi
async handler'da throw yoki rad etilgan await Ha — Express 5 da (Express 5 darsi); Express 4 da so'rov osilib qolardi
next(xato) Ha — callback kodidan xato uzatishning yo'li
setTimeout yoki callback ichida throw Yo'q — Express'dan tashqarida; jarayon o'ladi

Oxirgi qatorni kanon tajribasi (t34-halokat.txt) tasdiqladi: setTimeout(() => { throw … }) — Express ushlamadi, jarayon exit 1 bilan tugadi. Bu keyingi darsning mavzusi.

4.2 next ga nima berilsa — xato?

next() — keyingisiga o'tish. next(x) — x xato. Ikkita istisno bor: next("route") va next("router") — ular xato emas, middleware darsida ko'rgan "keyingi marshrutga o'tish" buyruqlari. Biz tekshirdik:

text
6. next('route'): 200 text/html; charset=utf-8 | keyingi route

Yana bir tuzoq — Error bo'lmagan qiymat tashlash: throw "satr xato". Ishlovchiga keladi, lekin unda message ham, stack ham yo'q:

text
7. throw 'satr': 500 application/json; charset=utf-8 | {"tur":"string","instanceofError":false}

Shuning uchun ishlovchi parametrni unknown deb oladi va avval turini tekshiradi.

4.3 Ishlovchi ichidagi xato

Ishlovchining o'zi throw qilsa-chi? Express keyingi 4 argumentli ishlovchiga o'tadi:

text
4. ishlovchi ichida throw: 500 application/json; charset=utf-8 | {"ikkinchi":"ishlovchi ichida: birinchi"}

Keyingisi bo'lmasa — yana sukut HTML ishlovchi. Xulosa: xato ishlovchi iloji boricha oddiy bo'lsin va o'zi xato qilmasin.

5. Tartib: 404 va ishlovchi eng oxirida

5.1 Biz tekshirdik: ishlovchi marshrutdan oldin

js
app.use((err, req, res, next) => { res.status(500).json({}); });
app.get("/", () => { throw new Error("kech qoldi"); });
text
3. ishlovchi oldin: 500 text/html; charset=utf-8 | <!DOCTYPE html> … <pre>Error: kech qoldi<br> …

Zanjir yuqoridan pastga yuradi. Xato marshrutda paydo bo'lganda, ishlovchi allaqachon orqada qolgan. Express faqat keyingi qatlamlarga qaraydi.

5.2 404 ham xato

Hech bir marshrut mos kelmasa, Express o'zining HTML "Cannot GET /yoq" sahifasini beradi. Bizga JSON kerak. Yechim — marshrutlardan keyin oddiy middleware, u faqat bitta ish qiladi:

ts
/** Hech bir marshrut mos kelmadi — routerlardan keyin */
export const topilmadi: RequestHandler = (req) => {
  throw new TopilmadiXatosi(
    `${req.method} ${req.path} — bunday manzil yo'q`);
};

U javob yozmaydi — xato tashlaydi va ishni ishlovchiga topshiradi. TopilmadiXatosi ga kod berilmagan, shuning uchun type: "about:blank". Kanon ilova.ts dagi tartib:

flowchart TD
  A[sorovId, helmet, cors] --> B[static, json]
  B --> C["/v1 routerlar"]
  C --> D[eskirgan yo'llar]
  D --> E[topilmadi — 404]
  E --> F[xatoIshlovchi]
  C -. throw .-> F
  B -. 400, 413 .-> F

Diagrammada uzuq chiziqlar — xato bilan "sakrash": qayerda bo'lmasin, to'g'ridan xatoIshlovchi ga.

Bitta so'rovni boshidan oxirigacha kuzataylik: GET /v1/vazifalar/77, bunday vazifa yo'q.

Qadam Middleware Natija
1 sorovId X-Request-Id berildi, next()
2 helmet, cors xavfsizlik sarlavhalari qo'yildi, next()
3 express.static public/ da bunday fayl yo'q — next()
4 express.json() GET, tana yo'q — next()
5 idempotentlik faqat POST'da ishlaydi — next()
6 /v1/vazifalar router: tekshirib params.id = 77, to'g'ri
7 handler → xizmat.ol(77) TopilmadiXatosi tashlandi
8 eskirgan yo'llar, topilmadi chetlab o'tildi — xato bor, oddiy middleware chaqirilmaydi
9 xatoIshlovchi 404 Problem Details, kod: "vazifa-topilmadi"

8-qadamga e'tibor bering. Xato tashlangandan keyin Express 3 argumentli middleware'larni o'tkazib yuboradi. U birinchi 4 argumentli ishlovchini qidiradi. GET /yoq esa boshqa yo'ldan yuradi: hech bir router mos kelmaydi, so'rov 8-qadamdagi topilmadi ga yetadi va u TopilmadiXatosi tashlaydi — natija yana 9-qadamda, lekin type: "about:blank" bilan.

Tekshirib ko'ring: topilmadi ni /v1 routerdan oldin qo'ysangiz nima bo'ladi?

Javob

Har so'rov 404 oladi — hatto GET /v1/vazifalar ham. topilmadi hech qanday shart tekshirmaydi, u o'ziga yetib kelgan har so'rovga xato tashlaydi. U ishlashi uchun undan oldingi hamma marshrutlar "imkoniyatini" olgan bo'lishi kerak.

6. Kanon: src/umumiy/xato-ishlovchi.ts

6.1 Uch tarmoq

Holat 34 da ishlovchi ilova.ts dan alohida faylga chiqdi. To'liq fayl (kanon; qatorlar telefon uchun Prettier bilan bo'lindi, kod aynan):

ts
// xato-ishlovchi.ts — global xato ishlovchi (4 argumentli middleware,
// eng oxirida): hamma xato bir joyda Problem Details'ga aylanadi
import type { ErrorRequestHandler, RequestHandler } from "express";
import { IlovaXatosi, TopilmadiXatosi } from "./xatolar.ts";
import { problemYasa, problemYubor } from "./problem.ts";

// body-parser xatolari (`type` maydoni) → o'zbekcha tafsilot
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",
  "charset.unsupported": "Belgilar to'plami qo'llab-quvvatlanmaydi",
};

/** http-errors (body-parser, serve-static): status + expose */
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"
  );
}

/** Hech bir marshrut mos kelmadi — routerlardan keyin */
export const topilmadi: RequestHandler = (req) => {
  throw new TopilmadiXatosi(
    `${req.method} ${req.path} — bunday manzil yo'q`,
  );
};

export type XatoLogi = (sorovId: string, xato: unknown) => void;

export function xatoIshlovchi(
  logla: XatoLogi = (id, x) => {
    console.error(`[${id}]`, x);
  },
): ErrorRequestHandler {
  return (xato: unknown, req, res, next) => {
    if (res.headersSent) {
      // javob yarmida — status o'zgartirib bo'lmaydi; Express o'zi
      // ulanishni uzadi
      logla(req.sorovId, xato);
      next(xato);
      return;
    }
    const umumiy = {
      instance: req.originalUrl,
      sorovId: req.sorovId,
    };
    if (xato instanceof IlovaXatosi) {
      res.set(xato.httpSarlavhalar);
      problemYubor(
        res,
        problemYasa(xato.holat, {
          ...umumiy,
          kod: xato.kod,
          sarlavha: xato.sarlavha,
          detail: xato.message,
          xatolar: xato.xatolar,
        }),
      );
      return;
    }
    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 (dasturchi xatosi, DB uzilishi…): tafsilot (stack,
    // xabar) mijozga BERILMAYDI — faqat logga
    logla(req.sorovId, xato);
    problemYubor(
      res,
      problemYasa(500, {
        ...umumiy,
        detail: "Kutilmagan xato. sorovId bilan murojaat qiling.",
      }),
    );
  };
}

logla — parametr. Sukut — console.error; testda — xotiraga yozadigan funksiya; Pino darsida — haqiqiy logger. Ishlovchi qaysi logger ishlatilishini bilmaydi.

6.2 Ikkinchi tarmoq: begona xatolarni tanish

express.json() ichidagi body-parser va express.static ichidagi serve-static o'z xatolarini http-errors kutubxonasi bilan yaratadi. Ularda status (400, 413, 415, 416) va expose (mijozga ko'rsatsa bo'ladimi) maydonlari bor. Kanon ularni oldingi darsdagi "maydon bo'yicha tekshiruv" bilan taniydi — fayldagi httpErrorsmi; type bo'yicha o'zbekcha matn — TANA_XATOLARI.

x is … — TypeScript type guard: true qaytsa, keyingi kodda x.status ishlatish mumkin. status < 500 — begona kutubxonaning 5xx xatosi ham yashiriladi.

Bu tarmoq Express'ga kirishda topilgan haqiqiy xatoning davosi: holat 13 da Range: bytes=99999- so'roviga serve-static 416 tashlagan, ishlovchi esa uni tanimay 500 qaytargan edi.

Biz holat 34 ni ishga tushirib tekshirdik (Node 24.21.0):

text
$ curl -X POST $A/v1/vazifalar -H 'Content-Type: application/json' -d '{"matn": "Non'
{"type":"about:blank","title":"Bad Request","status":400,"detail":"Tana yaroqli JSON emas","instance":"/v1/vazifalar","sorovId":"d95a828a-2c1a-46c9-bd3b-0961afc5a082"}
$ curl -X POST $A/v1/vazifalar … (17 000 belgili matn)
{"type":"about:blank","title":"Payload Too Large","status":413,"detail":"Tana 16 KB dan katta","instance":"/v1/vazifalar","sorovId":"91e520aa-c885-46a7-914f-85345b45c991"}
$ curl $A/yoq?x=1
{"type":"about:blank","title":"Not Found","status":404,"detail":"GET /yoq — bunday manzil yo'q","instance":"/yoq?x=1","sorovId":"92e69b08-7ce1-427e-b673-74d6c48dd661"}

Uchalasi ham bir xil shaklda: type, title, status, o'zbekcha detail, instance, sorovId. Mijoz kodi (frontend) faqat bitta formatni o'qiydi.

6.3 Uchinchi tarmoq: 500 va yashirish

Biz holat 34 ga "buzuq ombor" ulab tekshirdik — ol() har safar Error("ulanish uzildi: parol=maxfiy") tashlaydi (kanon testidagi BuzuqOmbor bilan bir xil):

text
LOG [sinov-500-abcdef] Error: ulanish uzildi: parol=maxfiy
500 application/problem+json; charset=utf-8
{"type":"about:blank","title":"Internal Server Error","status":500,"detail":"Kutilmagan xato. sorovId bilan murojaat qiling.","instance":"/v1/vazifalar/1","sorovId":"sinov-500-abcdef"}

Birinchi qator — server logi, qolgani — mijoz javobi. Ular sorovId bilan bog'langan: foydalanuvchi "sinov-500-abcdef" ni aytadi, siz logdan aynan o'sha xatoni topasiz. Javobda parol, maxfiy, fayl yo'li, stack — hech biri yo'q.

Kanon buni test bilan mustahkamlagan (test/xato.test.ts):

ts
test("500: ichki xabar mijozga chiqmaydi, logda bor", async () => {
  const r = await fetch(`${asos}/v1/vazifalar/1`,
    { headers: { "X-Request-Id": "test-500-abcdef" } });
  assert.equal(r.status, 500);
  const matn = await r.text();
  assert.doesNotMatch(matn,
    /parol|maxfiy|ulanish uzildi|BuzuqOmbor|\.ts:\d+/);
  assert.ok(loglar.some((q) => q[0] === "[test-500-abcdef]"));
});

doesNotMatch — "javobda bu so'zlar yo'q" tekshiruvi. Bir kun kimdir ishlovchini "yaxshilab", detail: xato.message qo'shsa — test darhol qizaradi.

6.4 headersSent: javob yarmida xato

Ba'zan javobning bir qismi allaqachon ketgan bo'ladi — masalan, eksport yoki oqim (stream) javobida. Status qatori va sarlavhalar yuborilgan, endi 500 qo'yib bo'lmaydi. Biz tekshirdik: handler res.write(...) dan keyin next(xato) chaqirdi.

text
   headersSent: true
5. headersSent → next(err): fetch xato — terminated (UND_ERR_SOCKET)

Kanon ishlovchi bu holatda logga yozadi va next(xato) bilan Express'ning sukut ishlovchisiga beradi. U ulanishni uzadi — mijoz "terminated" oladi. Bu to'g'ri: yarim javobni "muvaffaqiyatli" deb qabul qilishdan ko'ra, uzilgan ulanish yaxshiroq.

Tekshirib ko'ring: headersSent tarmog'ida res.status(500).json(...) chaqirsangiz nima bo'ladi?

Javob

ERR_HTTP_HEADERS_SENT xatosi: sarlavhalar allaqachon yuborilgan, ularni qayta yozib bo'lmaydi. Bu xato ishlovchi ichida yangi xato chiqaradi. Kanon tajribasida (t10-req-res.txt) ikkinchi res.json() aynan shu xatoni berdi.

7. Boshqa xatolarni map qilish

7.1 Validatsiya — allaqachon tayyor

Zod darsidagi tekshir() Zod xatosini o'zi ValidatsiyaXatosi ga aylantiradi. Ishlovchi Zod'ni umuman bilmaydi. Bu yaxshi chegara: validatsiya kutubxonasini almashtirsangiz, ishlovchi o'zgarmaydi.

7.2 Baza xatolari — 26-qismda

SQL qismida baza ham o'z xatolarini beradi. Masalan, Postgres takrorlanmas qiymat buzilsa, code: "23505" qaytaradi — bu 409 bo'lishi kerak. Map qilish joyi — ishlovchi emas, ombor: u bazaning xatosini KonfliktXatosi ga aylantiradi. Shunda xizmat ham, ishlovchi ham bazani bilmaydi. Hozircha g'oya yetarli — kod 26-qismda.

8. Hujumchi nigohi

  • Razvedka. Hujumchi ataylab buzuq so'rovlar yuboradi: {"matn":, ulkan tana, ?limit=-1, /v1/vazifalar/%00. Maqsad — stack, kutubxona versiyasi, fayl yo'li. Bizning himoya: iyerarxiyadan tashqaridagi har xato — umumiy matn; x-powered-by o'chirilgan.
  • Xabardagi sirlar. DB drayveri xato xabariga ulanish satrini (parol bilan) qo'shishi mumkin. detail ga faqat IlovaXatosi ning o'z xabari tushadi — test buni qo'riqlaydi.
  • Log zaharlash. Mijoz X-Request-Id ga yangi qator belgisi yoki 10 KB matn yuborsa-chi? Kanon sorovId faqat /^[\w.-]{8,128}$/ ga mos bo'lsa qabul qiladi, aks holda yangi UUID yasaydi (Express + TypeScript darsi).
  • Xatolar orqali sanash. "Bunday foydalanuvchi yo'q" va "parol noto'g'ri" ni farqlash — hujumchiga qaysi email ro'yxatda ekanini aytadi. Auth 28-qismda — u yerda bitta umumiy xabar ishlatiladi.

9. API qadami: global xato ishlovchi (holat 34, 5-qism)

Hali commit qilmang — commit keyingi darsda.

Fayl O'zgarish
src/umumiy/xato-ishlovchi.ts Yangi: topilmadi, xatoIshlovchi(logla), httpErrorsmi, TANA_XATOLARI
src/ilova.ts Ichki ishlovchi o'chdi; app.use(topilmadi) va app.use(xatoIshlovchi(xatoLogi))
src/ilova.ts → Bogliqliklar Yangi ixtiyoriy maydon xatoLogi?: XatoLogi
test/xato.test.ts Problem Details shakli (deepEqual), about:blank 404/405/415, 500 yashirish

src/ilova.ts — to'liq (kanon holat 34; Prettier bilan bo'lingan). Oxiridagi ikki qatorga qarang — topilmadi va xatoIshlovchi:

ts
// ilova.ts — Express ilovasi: middleware tartibi → routerlar → 404 →
// xato ishlovchisi.
// listen() YO'Q — server ishga-tushir.ts da, testlar ilovani portsiz
// ham ishlata oladi
import express from "express";
import cors from "cors";
import helmet from "helmet";
import { join } from "node:path";
import { sorovId } from "./umumiy/sorov-id.ts";
import {
  idempotentlik,
  XotiraIdempotentlikOmbori,
} from "./umumiy/idempotentlik.ts";
import type { IdempotentlikOmbori } from "./umumiy/idempotentlik.ts";
import { JSON_TURLARI } from "./umumiy/json-kerak.ts";
import { eskirgan, ESKI_YOLLAR } from "./umumiy/eskirgan.ts";
import {
  topilmadi,
  xatoIshlovchi,
} from "./umumiy/xato-ishlovchi.ts";
import type { XatoLogi } from "./umumiy/xato-ishlovchi.ts";
import { vazifaXizmati } from "./vazifalar/xizmat.ts";
import {
  teglarRouteri,
  vazifalarRouteri,
} from "./vazifalar/marshrut.ts";
import { eksportRouteri } from "./eksport/eksport.ts";
import type { VazifaOmbori } from "./vazifalar/ombor.ts";

export type Bogliqliklar = {
  ombor: VazifaOmbori;
  /**
   * CORS ruxsat etilgan origin'lar (frontend: Vite dev, Pages) —
   * bo'sh massiv: CORS sarlavhasi yo'q
   */
  corsOriginlar?: readonly string[];
  /** Idempotency-Key javoblari (27-qismda — Redis) */
  idempotentlikOmbori?: IdempotentlikOmbori;
  /**
   * kutilmagan xatolarni yozish (sukut — console.error; #42 da Pino)
   */
  xatoLogi?: XatoLogi;
};

export function ilovaYarat({
  ombor,
  corsOriginlar = [],
  idempotentlikOmbori = new XotiraIdempotentlikOmbori(),
  xatoLogi,
}: Bogliqliklar): express.Express {
  const app = express();
  app.disable("x-powered-by"); // "Express" ekanini oshkor qilmaymiz
  app.use(sorovId());
  // helmet: nosniff, HSTS, CSP va h.k. — JSON API uchun ham arzon
  // (t14: 1 paket, import 2 ms)
  app.use(helmet());
  // CORS: frontend boshqa origin'dan (GitHub Pages) — faqat
  // ro'yxatdagilar; preflight (OPTIONS) shu yerda javob oladi
  app.use(
    cors({
      origin: [...corsOriginlar],
      allowedHeaders: [
        "Content-Type",
        "Idempotency-Key",
        "X-Request-Id",
        "If-Match",
        "If-None-Match",
      ],
      exposedHeaders: [
        "Location",
        "X-Request-Id",
        "Idempotency-Replayed",
        "ETag",
        "Link",
        "Deprecation",
        "Sunset",
      ],
      maxAge: 600,
    }),
  );
  app.use(
    express.static(join(import.meta.dirname, "..", "public")),
  );
  app.use(express.json({ limit: "16kb", type: JSON_TURLARI }));
  const xizmat = vazifaXizmati(ombor);
  // Versiya URI'da: /v1/... Buzuvchi o'zgarish (maydon nomi, javob
  // shakli) bo'lsa — /v2, v1 bir muddat yashaydi
  const v1 = express.Router();
  v1.use(
    "/vazifalar",
    idempotentlik(idempotentlikOmbori),
    vazifalarRouteri(xizmat),
  );
  v1.use("/teglar", teglarRouteri(xizmat));
  v1.use("/eksportlar", eksportRouteri(ombor));
  app.use("/v1", v1);
  // Eski versiyasiz yo'llar (holat 05–24) — ishlaydi, lekin eskirgan:
  // Deprecation + Sunset + vorisga havola
  app.use(eskirgan(ESKI_YOLLAR), v1);
  app.use(topilmadi);
  app.use(xatoIshlovchi(xatoLogi));
  return app;
}

test/xato.test.ts — to'liq (kanon holat 34):

ts
import { after, before, test } from "node:test";
import assert from "node:assert/strict";
import { ishgaTushir as ishga } from "../src/ishga-tushir.ts";
import { yop } from "../src/yopish.ts";
import { XotiraOmbori } from "../src/vazifalar/xotira-ombor.ts";
import { jsonYubor } from "./yordamchi.ts";
import type { Problem } from "../src/umumiy/problem.ts";

// Fake ombor: haqiqiy xotira ombori, lekin ol() kutilmagan xato
// tashlaydi (DB uzildi kabi)
class BuzuqOmbor extends XotiraOmbori {
  override ol(): Promise<undefined> {
    return Promise.reject(
      new Error("ulanish uzildi: parol=maxfiy"),
    );
  }
}

let asos = "";
let toxta: () => Promise<boolean>;
const loglar: unknown[][] = [];
const aslError = console.error;
before(async () => {
  console.error = (...x: unknown[]) => loglar.push(x);
  const { server, manzil } = await ishga({
    port: 0,
    bogliqliklar: { ombor: new BuzuqOmbor() },
  });
  asos = manzil;
  toxta = () => yop(server, 1000);
});
after(async () => {
  console.error = aslError;
  await toxta();
});

test("Problem Details: domen xatosi (422) — type, title, detail, kod, xatolar, instance", async () => {
  const r = await fetch(
    `${asos}/v1/vazifalar`,
    jsonYubor("POST", { matn: " " }),
  );
  assert.equal(r.status, 422);
  assert.equal(
    r.headers.get("content-type"),
    "application/problem+json; charset=utf-8",
  );
  const p = (await r.json()) as Problem;
  assert.deepEqual(p, {
    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: "/v1/vazifalar",
    kod: "validatsiya",
    xatolar: [
      {
        joy: "body.matn",
        xabar: "matn 1–200 belgi bo'lishi kerak",
      },
    ],
    sorovId: r.headers.get("x-request-id"),
  });
});

test("Problem Details: 404 marshrut (about:blank), 405 Allow bilan, 415", async () => {
  const yoq = (await (
    await fetch(`${asos}/yoq?x=1`)
  ).json()) as Problem;
  assert.equal(yoq.type, "about:blank");
  assert.equal(yoq.title, "Not Found");
  assert.equal(yoq.instance, "/yoq?x=1");
  const r = await fetch(`${asos}/v1/teglar`, { method: "DELETE" });
  assert.equal(r.status, 405);
  assert.equal(r.headers.get("allow"), "GET, HEAD");
  assert.equal(
    ((await r.json()) as Problem).title,
    "Method Not Allowed",
  );
  const t = await fetch(`${asos}/v1/vazifalar`, {
    method: "POST",
    headers: { "Content-Type": "text/plain" },
    body: "x",
  });
  assert.equal(((await t.json()) as Problem).status, 415);
});

test("500: ichki xabar mijozga chiqmaydi, logda sorovId bilan bor", async () => {
  const r = await fetch(`${asos}/v1/vazifalar/1`, {
    headers: { "X-Request-Id": "test-500-abcdef" },
  });
  assert.equal(r.status, 500);
  const matn = await r.text();
  assert.doesNotMatch(
    matn,
    /parol|maxfiy|ulanish uzildi|BuzuqOmbor|\.ts:\d+/,
  );
  assert.equal(
    (JSON.parse(matn) as Problem).sorovId,
    "test-500-abcdef",
  );
  assert.ok(
    loglar.some(
      (q) => q[0] === "[test-500-abcdef]" && q[1] instanceof Error,
    ),
  );
});

Tekshiring: npm run check.

Kesim tekshiruvi: tip 0, lint 0, testlar 35/35 — biz ishga tushirdik (Node 24.21.0). Kesim holat 34 nusxasidan yasaldi: keyingi darsning fayllari (jarayon.ts, test/jarayon.test.ts, qolip) olib tashlandi, server.ts holat 27 dagidek qoldi. Yakuniy holat 34 da (keyingi dars bilan) — 36/36.

10. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Ishlovchi hech qachon chaqirilmaydi 3 ta argument (err, req, res, next) — next qolsin
Javob HTML, stack bilan Ishlovchi marshrutlardan oldin yoki umuman yo'q Eng oxirida, topilmadi dan keyin
Hamma so'rov 404 topilmadi routerlardan oldin Routerlardan keyin
Yaroqsiz JSON → 500 Ishlovchi faqat o'z klasslarini taniydi httpErrorsmi tarmog'i
ERR_HTTP_HEADERS_SENT Javob yuborilgandan keyin res.json headersSent → next(xato)
Mijoz detail da SQL ko'rdi detail: xato.message hamma xatoga Faqat IlovaXatosi uchun
setTimeout dagi xato jarayonni o'ldirdi Express'dan tashqarida Promise + await yoki next(xato)

11. Mashqlar

Yechimlar kurs/mashqlar/25/33-xato-ishlovchi/ da.

1-mashq (oson): qaysi biri xato ishlovchi?

length qiymatini va Express uni xato ishlovchi deb biladimi — yozing.

  • (err, req, res, next) => {} — [:4], [:ha]
  • function (e, q, s) {} — [:3], [:yo'q]
  • (err, req, res, _next) => {} — [:4], [:ha]
  • (err, req, res, next = () => {}) => {} — [:3], [:yo'q]
Yechim
  • 4, ha — klassik shakl.
  • 3, yo'q — nom ahamiyatsiz, soni 3.
  • 4, ha — _next ham parametr; pastki chiziq faqat linter uchun ishora.
  • 3, yo'q — sukut qiymatli parametr va undan keyingilar length ga kirmaydi. Bu kam uchraydigan, lekin haqiqiy tuzoq: next ga sukut qiymat bersangiz, ishlovchi jim o'chadi.

2-mashq (o'rta): modelga 404 qo'shing

Darsdagi zanjir modelini oling. Zanjir oxirigacha yetib, hech kim javob bermagan bo'lsa (javob.holat hali 200, tana bo'sh), 404 xatosi bilan xato ishlovchilarga qaytadan o'tishni emas, oddiyroq yo'lni tanlang: ro'yxatga topilmadi qatlamini qo'shing. Ikkita so'rovni sinang: { yol: "/salom" } (marshrut javob beradi) va { yol: "/yoq" }.

Yechim
js
function zanjir(qatlamlar) {
  return (sorov) => {
    const javob = { holat: 200, tana: "" };
    let i = 0;
    let xato;
    function next(x) {
      if (x !== undefined) xato = x;
      const q = qatlamlar[i++];
      if (!q) return;
      const xatoUchun = q.length === 4;
      if (Boolean(xato) !== xatoUchun) return next();
      try {
        if (xato) q(xato, sorov, javob, next);
        else q(sorov, javob, next);
      } catch (e) {
        next(e);
      }
    }
    next();
    return javob;
  };
}

class TopilmadiXatosi extends Error {
  holat = 404;
}
const ilova = zanjir([
  (req, res, next) => {
    if (req.yol !== "/salom") return next();
    res.tana = "salom";
  },
  (req) => {
    throw new TopilmadiXatosi(`${req.yol} — bunday manzil yo'q`);
  },
  (err, req, res, next) => {
    res.holat = err.holat ?? 500;
    res.tana = err.holat ? err.message : "Kutilmagan xato";
  },
]);
console.log(ilova({ yol: "/salom" }));
console.log(ilova({ yol: "/yoq" }));

Konsolda:

text
{ holat: 200, tana: 'salom' }
{ holat: 404, tana: "/yoq — bunday manzil yo'q" }

topilmadi qatlami shart tekshirmaydi — unga yetib kelgan har so'rov "topilmagan". Marshrut javob bersa, next() chaqirmaydi va zanjir to'xtaydi. Node satrda ' bo'lgani uchun uni qo'shtirnoq bilan ko'rsatdi.

3-mashq (qiyin): problemga(xato, sorovId)

Express'siz, toza funksiya yozing: u har qanday unknown qiymatni olib, { status, title, detail, sorovId } qaytaradi. Qoidalar kanondagidek. Birinchi: IlovaXatosi — o'z holati va xabari. Ikkinchi: status (son, < 500) va expose: true bor Error — o'sha status va type bo'yicha o'zbekcha matn (entity.parse.failed → "Tana yaroqli JSON emas"), bo'lmasa o'z xabari. Uchinchi: qolgani — 500, umumiy matn. title — kichik jadvaldan (400, 404, 413, 500).

Yechim
js
const NOMLAR = {
  400: "Bad Request", 404: "Not Found",
  413: "Payload Too Large", 500: "Internal Server Error",
};
const TANA = { "entity.parse.failed": "Tana yaroqli JSON emas" };

class IlovaXatosi extends Error {
  constructor(holat, xabar) {
    super(xabar);
    this.holat = holat;
  }
}

function problemga(xato, sorovId) {
  let status = 500;
  let detail = "Kutilmagan xato. sorovId bilan murojaat qiling.";
  if (xato instanceof IlovaXatosi) {
    status = xato.holat;
    detail = xato.message;
  } else if (xato instanceof Error && typeof xato.status === "number"
      && xato.status < 500 && xato.expose === true) {
    status = xato.status;
    detail = TANA[xato.type] ?? xato.message;
  }
  return { status, title: NOMLAR[status] ?? "Xato", detail, sorovId };
}

const parse = Object.assign(new SyntaxError("Unexpected end"),
  { status: 400, expose: true, type: "entity.parse.failed" });
const ichki = Object.assign(new Error("pool tugadi"),
  { status: 503, expose: false });
for (const x of [new IlovaXatosi(404, "Vazifa 7 topilmadi"),
  parse, ichki, "satr"]) {
  console.log(problemga(x, "abc12345").status,
    problemga(x, "abc12345").detail);
}

Konsolda:

text
404 Vazifa 7 topilmadi
400 Tana yaroqli JSON emas
500 Kutilmagan xato. sorovId bilan murojaat qiling.
500 Kutilmagan xato. sorovId bilan murojaat qiling.

Uchinchisi — begona kutubxonaning 503 xatosi: status < 500 sharti uni yashirdi. To'rtinchisi — Error emas, satr: hech bir tarmoqqa tushmadi.

4-mashq: API qadami

Ishlovchini src/umumiy/xato-ishlovchi.ts ga chiqaring (topilmadi, xatoIshlovchi(logla)), ilova.ts ga xatoLogi bog'liqligini qo'shing. test/xato.test.ts da BuzuqOmbor (extends XotiraOmbori, ol() rad etadi) bilan 500 testini yozing: javobda parol yo'q, logda sorovId bor. npm run check — yashil (35/35). Commit qilmang.

Yechim

Kod — «Kanon» bo'limidagi to'liq xato-ishlovchi.ts va «API qadami» dagi ilova.ts va test/xato.test.ts. Testda console.error ni vaqtincha almashtirib, chaqiruvlarni massivga yig'ing (before/after da asliga qaytaring) yoki ishgaTushir ga xatoLogi bering — ikkinchisi tozaroq. Bu darsdan keyin 35/35; kanon holat 34 da testlar 36/36 (keyingi dars bilan).

12. Real ishda

  • Har Express loyihasining oxirida shu ikki qator bor: 404 va xato ishlovchi. Begona repoda birinchi qaraladigan joy.
  • NestJS'da xuddi shu g'oya — exception filter'lar; Next.js Route Handler'larida — o'zingiz yozadigan try/catch o'rami.
  • Xato ishlovchi monitoring bilan bog'lanadi: Sentry SDK'si (34-qism) aynan shu joyga o'z ishlovchisini qo'yadi.
  • Intervyu savollari: "Express xato middleware'ni qanday taniydi?", "Nega production'da stack ko'rsatilmaydi?", "Javob yarmida xato bo'lsa nima qilasiz?"

Xulosa

  • Xato ishlovchi — 4 argumentli middleware; Express uni fn.length bilan taniydi, nom bilan emas.
  • Tartib: routerlar → topilmadi (404) → xatoIshlovchi — eng oxirida.
  • Uch tarmoq: IlovaXatosi → o'z statusi; http-errors (status < 500, expose) → o'zbekcha matn; qolgani → 500, tafsilot faqat logga.
  • sorovId mijoz javobini server logi bilan bog'laydi; javobda stack, fayl yo'li, ichki xabar — hech qachon.
  • headersSent bo'lsa — log va next(xato): ulanish uziladi.
  • Express'ning sukut ishlovchisi development'da stack'ni mijozga ko'rsatadi — o'z ishlovchingiz majburiy.

Keyingi dars: Process darajasidagi xatolar va crash strategiyasi — Express ushlay olmagan xatolar (unhandledRejection, uncaughtException) va nega "let it crash" eng xavfsiz yo'l.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Express global error handler: 4 argumentli middleware va 404 ishlovchi — IlmHamroh