IlmHamroh
JavaScript Full-stack/25-qism. HTTP, Express va REST API14/51-dars21 daqiqa
Mundarija (27)

Express middleware'lari: json, cors, helmet, cookie-parser va boshqalar

Qisqacha: Har Express ilovasida deyarli bir xil middleware'lar uchraydi. Tanani express.json({ limit }) o'qiydi va katta tanani 413 bilan to'xtatadi. Boshqa origin'dagi frontend'ga cors ruxsat beradi, helmet esa 12 ta xavfsizlik sarlavhasini qo'yadi. Webhook imzosi uchun express.raw xom baytlarni saqlaydi. Har birining narxi bor: kanon o'lchovida siqish (compression) katta ro'yxatda serverni 45 % sekinlashtirdi. Shuning uchun vazifalar-api ga faqat helmet va cors kirdi.

Bu darsda

  • express.json, express.urlencoded, express.raw farqini va limit nima uchun kerakligini bilasiz.
  • cors ni ruxsat ro'yxati bilan sozlaysiz va preflight javobini o'qiysiz.
  • helmet qo'yadigan sarlavhalarni taniysiz.
  • cookie-parser, morgan, compression, express-rate-limit qachon kerakligini o'lchov bilan ayta olasiz.
  • Webhook imzosini xom tana bilan to'g'ri tekshirasiz.

Oldin bilishingiz kerak: Middleware va request lifecycle, express.Router va qatlamli tuzilma, Request body o'qish va hajm cheklovi, crypto 1: hash va HMAC.

1. Nega bu kerak?

Middleware darsida middleware'ni o'zingiz yozdingiz. Bu — (req, res, next) funksiyasi: so'rovni ko'radi, nimadir qo'shadi va keyingisiga uzatadi. Qo'lda body o'qish darsida esa tanani bo'laklab yig'ish, hajmni sanash va JSON'ni tahlil qilish qancha kod talab qilishini ko'rdingiz.

Har loyihada shu ishlarni qayta yozmaslik uchun tayyor middleware'lar bor. Ular aeroportdagi nazorat punktlariga o'xshaydi. Biri yukni o'lchaydi (limit), biri pasportni tekshiradi (cors), biri hammaga ogohlantirish lentasi taqadi (helmet). Har punkt yo'lovchini biroz ushlab turadi — shuning uchun keraksiz punkt qo'yilmaydi.

Bu darsda o'nta mashhur middleware'ni ko'ramiz. Har biri uchun uch savol: nima qiladi, qanday sozlanadi, vazifalar-api ga kerakmi.

2. Tanani o'qiydiganlar: json, urlencoded, raw

2.1 express.json({ limit })

Siz uni Express'ga kirish darsidan beri ishlatyapsiz. Ichida — body-parser kutubxonasi: u tanani o'qiydi, hajmini sanaydi va JSON.parse qiladi. Asosiy sozlamalar:

Sozlama Sukut Ma'nosi
limit "100kb" Bundan katta tana — 413 xato
type "application/json" Qaysi Content-Type larni o'qisin
strict true Faqat obyekt va massiv (oddiy "salom" emas)

Biz uchta so'rov yubordik va xato obyektining maydonlarini chiqardik:

text
json 120KB: 413 {"type":"entity.too.large","status":413,"expose":true,"message":"request entity too large"}
json buzuq: 400 {"type":"entity.parse.failed","status":400,"expose":true,"message":"Expected double-quoted property name in JSON at position 15 (line 1 column 16)"}
json text/plain: 200 {"olindi":0}

Uch saboq:

  1. 120 KB tana sukut 100kb chegarasidan oshdi — 413 (Payload Too Large, "yuk juda katta"). Tana o'qilmadi, handler chaqirilmadi. Limit Content-Length bo'lmagan chunked tanada ham ishlaydi — baytlar o'qilgan sari sanaladi. Kanon testi ikkalasini ham tekshiradi ("413: Content-Length bilan va chunked"); bu chegarani qo'lda yozganingizni body o'qish darsida ko'rgansiz.
  2. Oxirida ortiqcha vergul bor JSON — 400. Xabar: "15-pozitsiyada qo'shtirnoqli xususiyat nomi kutilgan edi". Xato obyektidagi type maydoni bo'yicha uni ajratib olasiz.
  3. text/plain bilan yuborilgan JSON — xato ham yo'q, tana ham yo'q. Express 5 da bunday so'rovda req.body — undefined. Bu tuzoqni req obyekti darsida ko'rgandik.

vazifalar-api limitni kichraytiradi: express.json({ limit: "16kb" }). Bitta vazifa matni 200 belgigacha — 16 KB yetib ortadi. Kichik limit — hujumchi bir so'rov bilan server xotirasini to'ldira olmaydi.

Maslahat: limit ni ma'lumotingizga qarab tanlang. Rasm yuklash kerak bo'lsa, JSON limitini oshirmang — rasm uchun alohida marshrut va alohida middleware ishlatiladi.

2.2 express.urlencoded — HTML forma

Oddiy HTML <form> ma'lumotni application/x-www-form-urlencoded formatida yuboradi: ism=Ali&teg=uy. Uni express.urlencoded() o'qiydi. Asosiy sozlamasi extended:

text
forma extended:false: 200 {"ism":"Ali","teg":["uy","ish"],"manzil[shahar]":"Toshkent"}
forma extended:true:  200 {"ism":"Ali","teg":["uy","ish"],"manzil":{"shahar":"Toshkent"}}

Bir xil tana: ism=Ali&teg=uy&teg=ish&manzil[shahar]=Toshkent. extended: false (Express 5 sukuti) kvadrat qavsni oddiy belgi deb biladi. extended: true esa qs kutubxonasi bilan ichma-ich obyekt yasaydi.

Ichma-ich obyekt qulay ko'rinadi, lekin xavfli ham. Hujumchi manzil[__proto__][x]=1 yoki filtr[$ne]= kabi kutilmagan shakl yuborishi mumkin. Kerak bo'lmasa — extended: false. vazifalar-api faqat JSON qabul qiladi, shuning uchun urlencoded umuman ulanmagan.

2.3 express.raw — webhook uchun xom baytlar

Webhook — boshqa xizmat sizning serveringizga o'zi so'rov yuborishi: "to'lov o'tdi", "buyurtma bekor qilindi". Click, Payme, Telegram Bot API shunday ishlaydi. Xizmat so'rovga imzo qo'shadi: tananing HMAC'i (crypto darsi). Siz imzoni o'zingiz hisoblab, solishtirasiz — shunda so'rov haqiqatan o'sha xizmatdan kelganini bilasiz.

Tuzoq shu yerda. Imzo aynan kelgan baytlardan hisoblangan. express.json() tanani obyektga aylantiradi — asl baytlar yo'qoladi. Obyektni qayta JSON.stringify qilsangiz, bo'shliqlar o'zgaradi:

text
webhook raw:  200 {"isBuffer":true,"ok":true}
webhook json: 200 {"qayta":"{\"summa\":50000,\"buyurtma\":17}","ok":false}

Yuborilgan tana: {"summa": 50000, "buyurtma": 17} — ikki nuqtadan keyin bo'shliq bor. Qayta yig'ilgan satrda bo'shliq yo'q, demak HMAC boshqa, imzo "noto'g'ri" chiqdi. To'g'ri yo'l — webhook marshrutiga express.raw() qo'yish va uni express.json() dan oldin ulash:

js
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
const SIR = process.env.WEBHOOK_SIR ?? "";

// 1) webhook — xom Buffer; global json() dan OLDIN
app.post(
  "/webhook/tolov",
  express.raw({ type: "application/json", limit: "64kb" }),
  (req, res) => {
    const kutilgan = createHmac("sha256", SIR)
      .update(req.body) // Buffer — kelgan baytlar
      .digest("hex");
    const kelgan = req.get("x-imzo") ?? "";
    const togri =
      kelgan.length === kutilgan.length &&
      timingSafeEqual(Buffer.from(kelgan), Buffer.from(kutilgan));
    if (!togri) return res.sendStatus(401);
    const tolov = JSON.parse(req.body.toString("utf8"));
    res.json({ qabul: tolov.buyurtma });
  },
);

// 2) qolgan hamma marshrutlar — oddiy JSON
app.use(express.json({ limit: "16kb" }));

timingSafeEqual — solishtirish vaqti qaysi belgida farq borligini oshkor qilmasin (crypto darsi). Sir .env dan keladi, kodda yozilmaydi.

Imzo tekshiruvining o'zi — toza funksiya. Uni Web Crypto bilan brauzerda ham yozish mumkin. Quyidagi blokni «Ishga tushir» bilan sinang:

js
async function hmacHex(sir, matn) {
  const enc = new TextEncoder();
  const kalit = await crypto.subtle.importKey(
    "raw", enc.encode(sir),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
  );
  const imzo = await crypto.subtle.sign(
    "HMAC", kalit, enc.encode(matn));
  return [...new Uint8Array(imzo)]
    .map((b) => b.toString(16).padStart(2, "0")).join("");
}

const SIR = "test-sir-faqat-namuna";
const xom = '{"summa": 50000, "buyurtma": 17}';
const kelganImzo = await hmacHex(SIR, xom); // xizmat yuborgan

const qayta = JSON.stringify(JSON.parse(xom));
console.log("xom tana:   ", (await hmacHex(SIR, xom)) === kelganImzo);
const qaytaImzo = await hmacHex(SIR, qayta);
console.log("qayta yig'ilgan:", qaytaImzo === kelganImzo);
console.log(qayta);

Konsolda:

text
xom tana:    true
qayta yig'ilgan: false
{"summa":50000,"buyurtma":17}

Tekshirib ko'ring: Webhook marshrutini app.use(express.json()) dan keyin ulasangiz, express.raw() nima oladi?

Javob

Hech narsa. express.json() tanani allaqachon o'qib bo'lgan — oqim (stream) bir marta o'qiladi (Stream darsi). express.raw() bo'sh oqimni ko'radi va req.body ni o'zgartirmaydi: u obyekt bo'lib qoladi, Buffer emas. HMAC'ga obyekt berilsa, xato chiqadi. Shuning uchun xom tana kerak bo'lgan marshrut global json() dan oldin turadi.

3. Statik fayllar: express.static

express.static(papka) — static fayl server darsida qo'lda yozgan kodingizning tayyor varianti (ichida serve-static). U ETag, Last-Modified, Range va 304 javobini o'zi beradi. Foydali sozlamalar:

Sozlama Sukut Nima uchun
maxAge 0 Brauzer keshi (Cache-Control: max-age)
index "index.html" Papka so'ralganda qaysi fayl
dotfiles "ignore" .env kabi nuqtali fayllar berilmaydi
fallthrough true Fayl yo'q bo'lsa next() — keyingi marshrutga

vazifalar-api da public/index.html — API haqida qisqa sahifa. Kanon holat 13 da qo'lda yozilgan statik.ts ni o'chirib, express.static ga o'tgan edi. O'shanda bitta haqiqiy xato topilgan: Range: bytes=99999- so'roviga xato ishlovchimiz 500 qaytargan. Sabab — serve-static 416 statusli xato tashlaydi, ishlovchi esa uni tanimagan. Tuzatish — status < 500 && expose tarmog'i — middleware darsida xato ishlovchiga qo'shilgan va sizda allaqachon bor.

4. Xavfsizlik sarlavhalari va CORS

4.1 helmet — 12 ta sarlavha bir qatorda

helmet() javobga xavfsizlik sarlavhalarini qo'shadi. Kanon curl -i chiqishidan (holat 17), ularning bir qismi:

http
Content-Security-Policy: default-src 'self';base-uri 'self';…
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Referrer-Policy: no-referrer
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 0

Eng muhimlari:

  • X-Content-Type-Options: nosniff — brauzer Content-Type ni "taxmin qilmasin". Aks holda JSON ichidagi <script> HTML sifatida ishlab ketishi mumkin.
  • Strict-Transport-Security (HSTS) — brauzer bu saytga bir yil davomida faqat HTTPS orqali kirsin (HTTPS darsi).
  • Content-Security-Policy (CSP) — sahifa qayerdan skript yuklay olishini cheklaydi. JSON API uchun kamroq muhim, lekin public/index.html uchun foydali.
  • X-XSS-Protection: 0 — eski brauzer filtrini o'chiradi. Bu xato emas: o'sha filtrning o'zi zaiflik manbai bo'lgan.

Narxi: 1 paket, 0.1 MB, import 2 ms (kanon t14). Shuning uchun vazifalar-api ga kirdi.

Diqqat: helmet Cross-Origin-Resource-Policy: same-origin ham qo'yadi. Bu sarlavha <img> va <script> kabi no-cors so'rovlarga tegishli, fetch ga emas. Agar API'dan rasm bersangiz va uni boshqa saytda <img> bilan ko'rsatmoqchi bo'lsangiz — rasm bloklanadi. Bunday holatda helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } }).

4.2 cors — boshqa origin'dan kelgan frontend

CORS mijoz tomondan darsida brauzerdagi qoidani ko'rgansiz: http://localhost:5173 dagi sahifa http://localhost:3000 dagi API'ga fetch qilsa, brauzer javobni JS'ga faqat server ruxsat bersa beradi. Ruxsat — javobdagi Access-Control-Allow-* sarlavhalari. Ularni cors middleware'i qo'yadi.

vazifalar frontend'i GitHub Pages'da, API esa boshqa manzilda bo'ladi. Shuning uchun CORS kerak. Kanon sozlamasi:

ts
app.use(cors({
  origin: [...corsOriginlar], // env'dan: CORS_ORIGINS
  exposedHeaders: ["Location", "X-Request-Id"],
  maxAge: 600, // preflight javobi 10 daqiqa keshlanadi
}));
  • origin — ruxsat ro'yxati. Ro'yxatda bor origin kelsa, javobga aynan o'sha origin yoziladi.
  • exposedHeaders — brauzer JS'ga sukut bo'yicha faqat bir nechta oddiy sarlavhani ko'rsatadi. Location va X-Request-Id ni frontend o'qishi uchun ochiq e'lon qilamiz.
  • maxAge — brauzer har PATCH oldidan preflight yubormasin.

Preflight — brauzer "oddiy bo'lmagan" so'rovdan (PATCH, Content-Type: application/json) oldin yuboradigan OPTIONS so'rovi: "Shu metod va sarlavhalar bilan so'rasam maylimi?". Kanon curl'i (sarlavhalar qisqartirilgan):

bash
curl -i -X OPTIONS $A/vazifalar/1 -H 'Origin: http://localhost:5173' \
  -H 'Access-Control-Request-Method: PATCH' \
  -H 'Access-Control-Request-Headers: content-type'
http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Vary: Origin, Access-Control-Request-Headers
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Headers: content-type
Access-Control-Max-Age: 600

Begona origin (https://yomon.example) bilan oddiy GET esa 200 oladi — lekin Access-Control-Allow-Origin sarlavhasisiz. Diqqat qiling: server javob beradi, uni brauzer JS'ga bermaydi. CORS — server himoyasi emas, brauzer qoidasi. curl, Postman yoki boshqa server CORS'ga umuman qaramaydi.

sequenceDiagram
  participant B as Brauzer (5173)
  participant A as API
  B->>A: OPTIONS /vazifalar/1<br/>Origin, Request-Method: PATCH
  A-->>B: 204 + Allow-Origin, Allow-Methods
  B->>A: PATCH /vazifalar/1 (JSON)
  A-->>B: 200 + Allow-Origin
  Note over B: JS javobni o'qiy oladi

Diagrammada ikki bosqichga qarang: avval ruxsat so'raladi, keyin haqiqiy PATCH ketadi. maxAge: 600 bo'lsa, keyingi 10 daqiqada birinchi bosqich takrorlanmaydi.

Tekshirib ko'ring: Hamkasbingiz "CORS xatosi chiqyapti" deb cors({ origin: "*" }) qo'ydi. Nega bu yomon yechim bo'lishi mumkin va cookie bilan ishlaydigan API'da nima bo'ladi?

Javob

* — istalgan sayt brauzerdagi foydalanuvchi nomidan API javobini o'qiy oladi degani. Ochiq ma'lumot uchun bu normal, shaxsiy ma'lumot uchun — yo'q. Cookie bilan (credentials: true) brauzer * ni umuman qabul qilmaydi: CORS standarti credentials bilan aniq origin talab qiladi. Shuning uchun ba'zilar kelgan Origin ni "aks ettiradi" (origin: true) — bu esa har qanday saytga ruxsat beradi va eng xavfli variant. To'g'ri yo'l — ruxsat ro'yxati. Chuqur tafsilot — CORS chuqur darsida.

Brauzer cookie'larni bitta Cookie: til=uz; savat=… sarlavhasida yuboradi. cookie-parser uni req.cookies obyektiga aylantiradi. Unga sir bersangiz, imzolangan cookie'larni ham tekshiradi (req.signedCookies). Biz sinadik (cookie-parser 1.4.7):

text
set-cookie: [
  'til=uz; Max-Age=86400; Path=/; Expires=Sun, 25 Oct 2026 06:33:52 GMT; HttpOnly; SameSite=Lax',
  'savat=s%3A3.tGTqDeH7qmjLM3jTHElHBrlPnjLohfggvHnK6z65xI4; Path=/; HttpOnly'
]
cookie qaytib: 200 {"cookies":{"til":"uz"},"signedCookies":{"savat":"3"}}
cookie soxta: 200 {"cookies":{"til":"uz"},"signedCookies":{"savat":false}}

res.cookie("savat", "3", { signed: true }) qiymatga HMAC qo'shdi: s:3.<imzo>. Uchinchi so'rovda biz qiymatni 3 dan 9 ga o'zgartirdik — imzo mos kelmadi va cookie-parser false qaytardi.

Diqqat: imzo cookie'ni yashirmaydi. 3 hammaga ko'rinadi, faqat o'zgartirib bo'lmaydi. vazifalar-api hozir cookie ishlatmaydi. Cookie va sessiya auth qismida o'rganiladi, o'shanda kerak bo'ladi.

5.2 morgan — so'rov logi

morgan har so'rovga bitta qator yozadi. "dev" formatidagi haqiqiy qatorlar (biz ishga tushirdik):

text
POST /json 413 6.568 ms - 91
POST /json 400 9.095 ms - 148
POST /json 200 0.400 ms - 12

Metod, yo'l, status, vaqt va javob hajmi. "combined" formati — nginx loglari bilan bir xil. morgan oddiy matn yozadi. Serverda esa JSON log va so'rov ID'si kerak. Shuning uchun vazifalar-api keyinroq Pino'ga o'tadi (Logging asoslari) — morgan kirmadi.

5.3 compression — siqish

compression() javobni gzip yoki Brotli bilan siqadi (zlib darsi). 1 KB dan kichik javobga tegmaydi (threshold). Biz sinadik: 17 baytli javob siqilmadi, 100 vazifali ro'yxat br bilan keldi. Kanon t14 sarlavhalariga qarang: siqilgan javobga content-encoding bilan birga transfer-encoding qo'shildi. Siqilgan hajm oldindan noma'lum, shuning uchun Content-Length o'rniga chunked encoding ishlatiladi.

Kanon o'lchovi qiziqroq. 100 vazifali ro'yxat (14 751 bayt) Brotli bilan 420 baytga tushdi — 35 marta kichik. Lekin server sekundiga kamroq javob bera boshladi:

GET /royxat (100 vazifa): middleware bilan req/s
  • bo'sh express5 938 req/s
  • helmet5 277 req/s
  • cors5 689 req/s
  • compression420 bayt, br3 148 req/s
  • rate-limit4 883 req/s
  • morgan5 230 req/s
  • hammasi2 646 req/s

Manba: IlmHamroh kanon o'lchovi t14, 2026-10-24: express 5.3.0, Node 24.21.0, Windows 11, i5-12500H; autocannon 10 ulanish × 3 s, 2-tur; band mashina, server va yuk beruvchi bitta CPU'da

Grafik — ikkinchi tur. "compression" ustuniga qarang: /royxat bo'sh express'dan taxminan 45 % sekinroq (5 938 → 3 148 req/s). Siqish — CPU ishi, Node esa bitta asosiy thread'da ishlaydi (event loop darsi). Halol eslatma: birinchi turda farq atigi ~10 % edi (3 533 → 3 178). O'sha turda bo'sh express'ning o'zi past chiqdi. compression esa ikkala turda ham ~3 150–3 180 da turdi — uning "shifti" barqaror. Boshqa ustunlardagi farq — shovqin: ikki tur orasida bo'sh express 3 533 dan 5 938 gacha "sakradi".

Qaror: compression kirmadi. Siqishni Node'ning oldidagi reverse proxy (nginx) qiladi (reverse proxy darsi, to'liq — deploy qismida).

Kanondagi yana bir saboq: o'lchovning birinchi variantida yuk beruvchi (autocannon) server bilan bitta jarayonda edi. Raqamlar 3–10 marta past va ma'nosiz chiqdi (bo'sh express 5 094, helmet 1 337). Yuk beruvchi va server bitta event loop'ni bo'lishmasligi kerak.

5.4 express-rate-limit — so'rovlar chegarasi

Bir IP'dan daqiqasiga N tadan ortiq so'rovga 429 (Too Many Requests) qaytaradi. Biz limit: 2 bilan sinadik (express-rate-limit 8.7.1):

text
limit 1 200 x-ratelimit-limit: 2 | x-ratelimit-remaining: 1 | x-ratelimit-reset: 1792823704
limit 2 200 x-ratelimit-limit: 2 | x-ratelimit-remaining: 0 | x-ratelimit-reset: 1792823704
limit 3 429 retry-after: 60 | x-ratelimit-limit: 2 | x-ratelimit-remaining: 0 | x-ratelimit-reset: 1792823704

Uchinchi so'rov 429 va Retry-After: 60 oldi — "60 soniyadan keyin urinib ko'ring". standardHeaders: "draft-8" bersangiz, IETF qoralamasidagi yangi RateLimit va RateLimit-Policy sarlavhalari chiqadi.

Nega kanonga kirmadi? Uch sabab:

  1. Hisob jarayon xotirasida. Ikki nusxa ishga tushsa, har biri o'z hisobini yuritadi. Umumiy hisob uchun Redis kerak (Redis qismi).
  2. Proxy orqasida hamma so'rov bitta IP'dan keladi — trust proxy siz chegara noto'g'ri ishlaydi (reverse proxy darsi).
  3. Haqiqiy ehtiyoj — login va parol tiklashni himoyalash. U auth qismida paydo bo'ladi.

Narxiga ham qarang: express-rate-limit 67 paket o'rnatadi. Lekin uning 65 tasi — express ning o'zi: npm 7+ "peer" bog'liqlikni avtomatik o'rnatadi. O'z bog'liqliklari faqat ikkita (debug, ip-address).

Tekshirib ko'ring: compression kichik javobni (17 bayt) nega siqmadi? Bu yaxshimi?

Javob

compression sukut bo'yicha 1 KB dan kichik javobni siqmaydi (threshold). Bu to'g'ri: kichik javobda siqish sarlavhalari va CPU vaqti tejalgan baytdan qimmatroq. Siqilgan 17 bayt hatto kattalashishi mumkin.

6. Tartib muhim

Middleware'lar ulanish tartibida ishlaydi (middleware darsi). Noto'g'ri tartib — jim xato. vazifalar-api dagi tartib (holat 17 oxirida; birinchi qator — sorovId — Express + TypeScript darsida qo'shiladi):

flowchart TD
  A["sorovId — X-Request-Id"] --> B["helmet — sarlavhalar"]
  B --> C["cors — preflight shu yerda tugaydi"]
  C --> D["express.static — public/"]
  D --> E["express.json — 16 KB"]
  E --> F["/vazifalar routeri"]
  F --> G["404"]
  G --> H["xato ishlovchi"]

Yuqoridan pastga o'qing va har qadamda "nega aynan shu yerda?" deb so'rang:

  • sorovId birinchi — xato bo'lsa ham javobda ID bo'lsin.
  • helmet va cors erta — 404 va xato javoblarida ham sarlavhalar bo'lsin. Aks holda brauzer xato javobini o'qiy olmaydi va frontend "CORS xatosi" ko'radi, aslida esa 404 bo'lgan.
  • cors preflight OPTIONS ga o'zi 204 javob beradi — so'rov routerga yetmaydi.
  • express.static tanani o'qishdan oldin — statik fayl uchun JSON tahlil qilish shart emas.
  • Webhook bo'lsa — express.json dan oldin.

7. API qadami: helmet va CORS (holat 17, 1-qism)

Holat 17 to'rt darsga bo'lingan: bugun middleware'lar, keyin Express 5 yangiliklari, shablonlar (faqat tajriba) va Express + TypeScript. Hali commit qilmang — commit to'rtinchi darsda.

O'rnatish (kanondagi aniq versiyalar):

bash
npm i --save-exact helmet@8.3.0 cors@2.8.6
npm i -D --save-exact @types/cors@2.8.19

helmet o'z tiplari bilan keladi, cors uchun alohida @types/cors. src/ilova.ts dagi o'zgarish (kanon, qisqartirilgan):

diff
+import cors from "cors";
+import helmet from "helmet";
 …
-export type Bogliqliklar = { ombor: VazifaOmbori };
+export type Bogliqliklar = {
+  ombor: VazifaOmbori;
+  /** bo'sh massiv: CORS sarlavhasi yo'q */
+  corsOriginlar?: readonly string[];
+};
 …
   app.disable("x-powered-by");
+  app.use(helmet());
+  app.use(cors({
+    origin: [...corsOriginlar],
+    exposedHeaders: ["Location", "X-Request-Id"],
+    maxAge: 600,
+  }));
   app.use(express.static(join(import.meta.dirname, "..", "public")));

Ro'yxat env'dan keladi — src/server.ts dagi o'zgarish (kanon, qatorlar bo'lingan):

diff
 import { yop } from "./yopish.ts";
+import { XotiraOmbori } from "./vazifalar/xotira-ombor.ts";
 
 const { server, manzil } = await ishgaTushir({
   port: Number(process.env.PORT ?? 3000),
   host: process.env.HOST ?? "127.0.0.1",
+  bogliqliklar: {
+    ombor: new XotiraOmbori(),
+    // CORS_ORIGINS="http://localhost:5173,https://login.github.io"
+    corsOriginlar: (process.env.CORS_ORIGINS ?? "")
+      .split(",").map((o) => o.trim()).filter((o) => o !== ""),
+  },
 });

Testlar ham frontend origin'ini beradi — test/yordamchi.ts:

diff
+import { XotiraOmbori } from "../src/vazifalar/xotira-ombor.ts";
+
+export const FRONTEND = "http://localhost:5173";
 …
-  const { server, manzil } = await ishga({ port: 0 });
+  const { server, manzil } = await ishga({
+    port: 0,
+    bogliqliklar: {
+      ombor: new XotiraOmbori(), corsOriginlar: [FRONTEND],
+    },
+  });

Tipli config obyekti env validatsiyasi darsida keladi. Ilova corsOriginlar ni parametr sifatida oladi — testlar uni o'zi beradi, process.env ga tegmaydi.

Xato ishlovchi bugun o'zgarmaydi. serve-static (416, 403) va body-parser xatolari http-errors shaklida keladi: status va expose maydonlari bor (expose: true — xabarni mijozga ko'rsatsa bo'ladi). Ularni taniydigan status < 500 && expose tarmog'i middleware darsidan beri bor. Express + TypeScript darsida bu uzun shart httpErrorsmi degan type guard'ga ajratiladi — ma'nosi o'sha.

Yangi test fayli test/middleware.test.ts — preflight, ruxsat etilgan va begona origin, helmet (kanon, qatorlar bo'lingan):

ts
import { after, before, test } from "node:test";
import assert from "node:assert/strict";
import { FRONTEND, ishgaTushir, xomSorov } from "./yordamchi.ts";

let asos = "";
let yop: () => Promise<boolean>;
before(async () => ({ asos, yop } = await ishgaTushir()));
after(() => yop());

test("CORS: ruxsat etilgan origin — preflight 204 va sarlavhalar; "
  + "begona origin — sarlavhasiz", async () => {
  const pre = await xomSorov(`${asos}/vazifalar`, {
    metod: "OPTIONS",
    sarlavhalar: {
      Origin: FRONTEND,
      "Access-Control-Request-Method": "PATCH",
      "Access-Control-Request-Headers": "content-type",
    },
  });
  const s = pre.sarlavhalar;
  assert.equal(pre.holat, 204);
  assert.equal(s["access-control-allow-origin"], FRONTEND);
  assert.match(String(s["access-control-allow-methods"]), /PATCH/);
  assert.equal(s["access-control-max-age"], "600");
  const oddiy = await xomSorov(`${asos}/vazifalar`, {
    sarlavhalar: { Origin: FRONTEND },
  });
  assert.equal(
    oddiy.sarlavhalar["access-control-allow-origin"], FRONTEND);
  assert.equal(oddiy.sarlavhalar["access-control-expose-headers"],
    "Location,X-Request-Id");
  const begona = await xomSorov(`${asos}/vazifalar`, {
    sarlavhalar: { Origin: "https://yomon.example" },
  });
  // server javob beradi — brauzer uni JS'ga bermaydi
  assert.equal(begona.holat, 200);
  assert.equal(
    begona.sarlavhalar["access-control-allow-origin"], undefined);
});

test("helmet sarlavhalari va x-powered-by yo'q", async () => {
  const r = await xomSorov(`${asos}/vazifalar`);
  assert.equal(r.sarlavhalar["x-content-type-options"], "nosniff");
  assert.ok(r.sarlavhalar["content-security-policy"]);
  assert.equal(r.sarlavhalar["x-powered-by"], undefined);
});

(Faqat format o'zgardi: uzun qatorlar bo'lingan, pre.sarlavhalar — s qisqa nomga olingan.)

Natija: prod bog'liqliklar 1 → 3 (daraxtda 65 → 68 paket). Bugungi kesim — holat 13 + helmet, cors va ikki test (CORS, helmet). Biz uni holat 17 nusxasidan keyingi darslar o'zgarishlarini olib tashlab tekshirdik (Node 24.21.0): tip 0, lint 0, testlar 9/9.

8. Hujumchi nigohi

Hujum Qanday Himoya
Katta tana 100 MB JSON yuborib xotirani to'ldirish express.json({ limit: "16kb" }) → 413
Soxta webhook "To'lov o'tdi" so'rovini o'zi yuborish HMAC imzo, xom tana, timingSafeEqual
Imzoni buzish Qayta yig'ilgan JSON bilan tekshiruv "ishlamaydi" — dasturchi tekshiruvni o'chiradi express.raw — tekshiruvni o'chirmaslik
CORS aks ettirish Istalgan origin'ga ruxsat Ruxsat ro'yxati
MIME taxmini JSON ichidagi HTML'ni brauzer bajaradi nosniff (helmet)
Ichma-ich forma a[__proto__][x]=1 extended: false, oq ro'yxat

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
req.body — undefined Content-Type JSON emas yoki express.json() ulanmagan Turini tekshiring (415)
request entity too large Tana limit dan katta Limitni ataylab tanlang
Webhook imzosi doim noto'g'ri express.json() oldin o'qigan express.raw() oldinroq
Frontend'da "CORS xatosi", aslida 404 cors routerdan keyin ulangan cors ni erta ulang
Location sarlavhasi JS'da null exposedHeaders da yo'q exposedHeaders: ["Location"]
Rasm boshqa saytda ko'rinmaydi helmet CORP same-origin crossOriginResourcePolicy

10. Mashqlar

Yechimlar kurs/mashqlar/25/14-middleware/ da.

1-mashq (oson): to'ldiring

  • express.json() sukut limiti —
  • Tana limitdan katta bo'lsa status —
  • Webhook imzosini tekshirish uchun xom Buffer beradigan middleware — express.
  • Preflight so'rovining HTTP metodi —
  • JS'ga Location sarlavhasini ko'rsatish uchun cors sozlamasi —
Yechim
  • 100kb — body-parser sukuti.
  • 413 — Payload Too Large; xato obyektida type: "entity.too.large".
  • raw — express.raw({ type: "application/json" }), global json() dan oldin.
  • OPTIONS — brauzer o'zi yuboradi, cors 204 bilan javob beradi.
  • exposedHeaders — brauzer sukut bo'yicha faqat oddiy sarlavhalarni ko'rsatadi.

2-mashq (o'rta): imzo tekshiruvchi

Web Crypto bilan imzoTogrimi(sir, xomMatn, kelganHex) funksiyasini yozing. U true/false qaytarsin. kelganHex uzunligi 64 bo'lmasa yoki hex bo'lmasa — false, hisoblamasdan. Uch holatni sinang: to'g'ri imzo, qayta yig'ilgan JSON, buzuq satr. Toza JS — «Ishga tushir» bilan ishlaydi. Yordam: hmacHex — «Tanani o'qiydiganlar» bo'limida; satrni regex bilan tekshirish — RegExp darsi.

Yechim
js
async function hmacHex(sir, matn) {
  const enc = new TextEncoder();
  const kalit = await crypto.subtle.importKey(
    "raw", enc.encode(sir),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
  );
  const imzo = await crypto.subtle.sign(
    "HMAC", kalit, enc.encode(matn));
  return [...new Uint8Array(imzo)]
    .map((b) => b.toString(16).padStart(2, "0")).join("");
}

async function imzoTogrimi(sir, xomMatn, kelganHex) {
  if (!/^[0-9a-f]{64}$/.test(kelganHex)) return false;
  return (await hmacHex(sir, xomMatn)) === kelganHex;
}

const SIR = "test-sir-faqat-namuna";
const xom = '{"summa": 50000, "buyurtma": 17}';
const imzo = await hmacHex(SIR, xom);
console.log(await imzoTogrimi(SIR, xom, imzo));
const qayta = JSON.stringify(JSON.parse(xom));
console.log(await imzoTogrimi(SIR, qayta, imzo));
console.log(await imzoTogrimi(SIR, xom, "salom"));

Konsolda:

text
true
false
false

Serverda solishtirishni timingSafeEqual bilan qiling. Web Crypto'da buning o'rniga crypto.subtle.verify("HMAC", kalit, imzoBaytlari, matn) bor — u ham vaqtga chidamli solishtiradi.

3-mashq (qiyin): webhook marshruti

Express ilovasini yozing: POST /webhook — express.raw + HMAC tekshiruvi (to'g'ri — 200 {"qabul":17}, noto'g'ri — 401), qolgan marshrutlar uchun express.json({ limit: "1kb" }) va POST /echo (tanani qaytaradi). Ilovani listen(0) bilan ishga tushirib, fetch bilan to'rt holatni tekshiring: to'g'ri imzo, noto'g'ri imzo, /echo kichik tana, /echo 2 KB tana (413). Oxirida close(). Express kerak — o'z loyihangizda ishga tushiring.

Yechim
js
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const SIR = "test-sir-faqat-namuna";
const hmac = (b) => createHmac("sha256", SIR).update(b).digest("hex");

const app = express();
app.post("/webhook", express.raw({ type: "application/json" }),
  (req, res) => {
    const kelgan = Buffer.from(req.get("x-imzo") ?? "");
    const kutilgan = Buffer.from(hmac(req.body));
    const ok = kelgan.length === kutilgan.length &&
      timingSafeEqual(kelgan, kutilgan);
    if (!ok) return res.sendStatus(401);
    res.json({ qabul: JSON.parse(req.body).buyurtma });
  });
app.use(express.json({ limit: "1kb" }));
app.post("/echo", (req, res) => res.json(req.body));
app.use((err, _req, res, _next) => {
  res.status(err.status ?? 500).json({ xato: err.type });
});

const server = app.listen(0, "127.0.0.1");
await new Promise((ok) => server.once("listening", ok));
const A = `http://127.0.0.1:${server.address().port}`;
const yubor = (yol, body, imzo = "") => fetch(A + yol, {
  method: "POST", body,
  headers: { "content-type": "application/json", "x-imzo": imzo },
});
const xom = '{"summa": 50000, "buyurtma": 17}';
for (const r of [
  await yubor("/webhook", xom, hmac(xom)),
  await yubor("/webhook", xom, hmac("boshqa")),
  await yubor("/echo", '{"a":1}'),
  await yubor("/echo", JSON.stringify({ m: "x".repeat(2048) })),
]) console.log(r.status, await r.text());
server.close();

Konsolda (biz ishga tushirdik, express 5.3.0, Node 24.21.0):

text
200 {"qabul":17}
401 Unauthorized
200 {"a":1}
413 {"xato":"entity.too.large"}

JSON.parse(req.body) — Buffer ni ham qabul qiladi (avval satrga aylantiradi).

4-mashq: API qadami

vazifalar-api ga helmet va cors ni qo'shing (aniq versiyalar), corsOriginlar ni Bogliqliklar ga va CORS_ORIGINS ni server.ts ga ulang. test/middleware.test.ts ga ikki test yozing: preflight (204, PATCH ruxsat, Max-Age: 600) va helmet (nosniff bor, x-powered-by yo'q). npm run check — yashil. Commit qilmang.

Yechim

Namuna — «API qadami» bo'limidagi diff va test. xomSorov — test/yordamchi.ts dagi node:http yordamchisi (holat 13 dan): shartli so'rovlar uchun fetch emas, xom so'rov kerak edi. Testda frontend origin'i FRONTEND konstantasi, ishgaTushir unga corsOriginlar: [FRONTEND] beradi. Testlar 7 → 9. Kanonning middleware.test.ts dagi uchinchi test (X-Request-Id) Express + TypeScript darsida qo'shiladi va soni 10 bo'ladi.

11. Real ishda

  • Har Express loyihasida express.json({ limit }), helmet, cors — standart uchlik. Intervyuda "cors nima qiladi va u server himoyasimi?" — mashhur savol.
  • To'lov tizimlari (Click, Payme, Stripe) va GitHub webhook'lari — xom tana va HMAC. Imzo tekshiruvi bo'lmagan webhook — "bepul to'lov" teshigi.
  • Siqish va rate-limit ko'pincha nginx yoki bulut xizmatida (Cloudflare) qilinadi, Node'da emas.
  • morgan eski loyihalarda ko'p uchraydi. Yangi loyihalarda Pino va pino-http.

Xulosa

  • express.json({ limit }) — tana o'qish va hajm chegarasi; xatolar type maydoni bilan (entity.too.large, entity.parse.failed).
  • Webhook imzosi uchun express.raw() — global json() dan oldin; qayta yig'ilgan JSON imzoni buzadi.
  • helmet — 12 sarlavha, 1 paket; cors — ruxsat ro'yxati, exposedHeaders, preflight 204.
  • CORS — brauzer qoidasi, server himoyasi emas: begona origin ham 200 oladi.
  • Har middleware'ning narxi bor: compression /royxat da −45 %; rate-limit xotirada bitta jarayon uchun. vazifalar-api ga faqat helmet va cors kirdi.

Keyingi dars: Express 5: async xatolar va yangi route sintaksisi — Express 4 dan nima o'zgardi va qaysi eski kod endi ishga tushmaydi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Express middleware'lari: json, cors, helmet, cookie-parser va boshqalar — IlmHamroh