Skip to main content

โครง HTTP Server และ Middleware กลาง

ภาพรวม

Feature นี้เป็นกระดูกสันหลังของ service client-api ทั้งตัว ไม่ใช่ endpoint ทางธุรกิจ แต่เป็นชั้นที่ทุก endpoint ต้องวิ่งผ่าน ประกอบด้วยขั้นตอน bootstrap (โหลด config ต่อ dependency เปิด admin port และเฝ้าระวัง outage), การประกอบ gin engine, ลำดับ middleware กลาง, รูปแบบ error envelope ที่เข้ากันได้กับ NestJS เดิม และ rate limiter ระดับแอปพลิเคชัน

สิ่งที่ควรเข้าใจก่อนอ่าน feature อื่น: service นี้เป็นการ port ของ NestJS API เดิมแบบ 1:1 ทั้งรูปแบบ error, ข้อความ validation, การ serialize วันที่ (JSTime) และ route path ทั้งหมด เพื่อให้ทดแทน service Node เดิมได้โดยที่ client ไม่ต้องแก้ไขใด ๆ ทุกโดเมนเขียนด้วย pattern เดียวกันคือ entity.gorepository.goservice.gohandler.goregister.go และเขียน SQL ด้วยมือผ่าน sqlx โดยไม่ใช้ ORM

Business Flow

ขั้นตอน boot (app.New)

  1. config.Load อ่านค่าทั้งหมดจาก environment มาเป็น typed struct แล้วเรียก Validate() — config ที่เป็นไปไม่ได้จะทำให้ boot ล้มทันที (fail-fast) และไม่มีแพ็กเกจใดเรียก os.Getenv เองกลางโค้ด
  2. สร้าง dependency เฉพาะตัวที่ถูกตั้งค่าไว้ (auto-enable ตาม host variable) ได้แก่ sqlx pool (DB_HOST), named Redis client (REDIS_*) และ AMQP publisher (AMQP_URLS) แต่ละตัว implement contract 4 เมธอด Name/Connect/Ping/Close แล้วลงทะเบียนใน registry
  3. ConnectAll ทำงานภายใน budget 30 วินาที และปฏิเสธชื่อ dependency ที่ซ้ำกัน เนื่องจากชื่อคือ key ที่ใช้อ้างอิงใน readiness, metric และ alert
  4. เปิด health checker (ticker ping), admin server ที่พอร์ต :9100 (metrics/version/pprof) และ supervisor ที่เฝ้าระวัง dependency แต่ละตัวแยกกัน — เมื่อ dependency ล่มจะ retry ตามจังหวะ แจ้งเตือน chat webhook เมื่อครบ 1 นาที และเมื่อครบ 3 นาทีจะ drain แล้ว exit เพื่อให้ orchestrator รีสตาร์ต pod ใหม่
  5. cmd/api/main.go ประกาศ RabbitMQ topology (exchange, 4 queue และ DLQ) แบบ best-effort จากนั้นเรียก server.New(deps, ...Register) เพื่อประกอบ engine

ลำดับ middleware ของทุก request (server.New)

  1. MetricsMiddleware() — ชั้นนอกสุด วัด status และ latency ของทุก request โดย label เป็น route pattern ไม่ใช่ raw path เพื่อจำกัด cardinality
  2. middleware.CORSAllowAll() — parity กับ source เดิม คือกำหนด origin, methods และ headers เป็น * แบบไม่มีเงื่อนไข
  3. middleware.Helmet() — security header ชุด default ยกเว้น Cross-Origin-Resource-Policy เนื่องจาก source ตั้งค่า crossOriginResourcePolicy: false
  4. httpx.ExceptionMiddleware — แปลง error และ panic ทุกชนิดเป็น envelope {"statusCode","message","error"} โดยฟิลด์ message เป็น string หรือ array ของ string ได้ และ error เป็น HTTP reason phrase ส่วน error ที่ไม่ใช่ *httpx.Exception จะกลายเป็น 500 พร้อมข้อความ System Failure
  5. เฉพาะสภาพแวดล้อม non-production จะเพิ่ม gin.Logger() ไว้ชั้นหน้าสุด

middleware เพิ่มเติมบน group /api

  • clsx.Middleware() — สร้าง request-scoped store ประกอบด้วย transactionId (จาก header X-Request-Id หรือสุ่ม hex 12 ตัว), sessionId (segment ที่ 3 ของ Authorization ถ้ามี) และ lang (จาก x-lang) จากนั้นสะท้อน X-Request-Id กลับใน response header
  • middleware.AppRateLimit — Redis-backed limiter ที่ใช้แทน ThrottlerModule เดิมซึ่งเป็น in-memory ต่อ pod โดย key ด้วย client IP ที่อ่านตามลำดับ x-forwarded-for[0]x-real-ipx-client-ip → socket มี sliding window (RATE_LIMIT_APP_TTL), limit (RATE_LIMIT_APP_LIMIT) และ block duration (RATE_LIMIT_BLOCK_DURATION) เมื่อเกิน limit จะตั้ง block key และตอบ 429 ทันทีในทุก request ถัดไปโดยไม่นับเพิ่ม ทั้งนี้ Redis error ทุกกรณีจะ fail open (ปล่อยผ่าน) เพื่อไม่ให้ปฏิเสธ traffic เพียงเพราะ storage มีปัญหา

Business rule ที่ควรจำ

  • route ที่ไม่รู้จักจะถูก NoRoute ตอบกลับเป็น 404 envelope พร้อมข้อความรูปแบบ Cannot <METHOD> <path>
  • /livez, /healthz และ /readyz mount ที่ root ของ business port ไม่ได้อยู่ใต้ /api เพราะ k8s probe และ ALB healthcheck ยิงเข้ามาที่พอร์ตนี้ ส่วน /metrics และ pprof อยู่บนพอร์ต :9100 เท่านั้นและห้าม expose ออกไป
  • /livez ตรวจสอบเฉพาะตัว process โดย dependency ต้องไม่ทำให้ liveness fail เพราะการรีสตาร์ต pod ไม่ช่วยอะไรหาก database ล่ม ขณะที่ /readyz ผูกกับผล ping ของ dependency ทุกตัวรวมถึงสถานะ draining
  • ลำดับ shutdown: cancel context → หยุดรับ request → เรียก SetDraining() ให้ readiness fail ก่อนเพื่อให้ load balancer ถอน pod ออก → ปิด admin server → ปิด dependency ย้อนลำดับ ทั้งหมดถูกจำกัดด้วย SHUTDOWN_TIMEOUT
  • deps.DB, deps.Redis และ deps.AMQP เป็น nil ได้ทั้งหมด ทุกฟังก์ชัน Register จึงต้อง nil-safe เพื่อให้ boot ได้แม้ไม่มี backend โดย rate limiter และ cache จะกลายเป็น no-op

ไฟล์และฟังก์ชันหลัก

ไฟล์บทบาท
cmd/api/main.gobinary SVC=api เรียก app.New ประกาศ AMQP topology ประกอบ engine และทำ graceful shutdown
internal/app/app.goapp.New(ctx, cancel, service, role) — bootstrap ร่วมของ api และ worker
internal/server/server.goserver.New(deps, registers...) ประกอบ gin engine และลำดับ middleware พร้อมนิยาม Deps และ RegisterFunc
internal/server/metrics_middleware.goMetricsMiddleware()
internal/middleware/cors.go, helmet.goCORSAllowAll(), Helmet()
internal/middleware/ratelimit.goAppRateLimit(rdb, ttlMs, limit, blockMs), RouteRateLimit(rdb, limit, windowSecs)
internal/middleware/throttler.goRateLimit(perMinute) — limiter รายนาทีที่ public-content ใช้
internal/httpx/exception.goException และ constructor New/BadRequest/Unauthorized/Forbidden/NotFound/Conflict/InternalServerError
internal/httpx/abort.goAbort(c, err), ExceptionMiddleware(sentry, logger)
internal/httpx/validate.go, checks.goBindAndValidate, Rule, Check (mirror ของ class-validator)
internal/httpx/jstime.goJSTime — serialize วันที่ในรูปแบบ JS Date
internal/clsx/clsx.goMiddleware() และ Store (transactionId/sessionId/lang)
internal/deps/*contract Dependency, Registry และ subpackage ต่อ backend
internal/supervisor/supervisor.goStart(...) และ evaluate(downFor, failures, alerted, cfg) ซึ่งเป็น pure function
internal/config/config.go, api.go, api_load.gotyped Config ทั้งหมด

Route ที่ประกอบไว้ในชั้นนี้ ได้แก่ GET /livez, GET /healthz และ GET /readyz ที่ root ส่วน /api และ /api/health ดูรายละเอียดได้ที่ Health Check และ Probe

จุดเชื่อมต่อกับ Service อื่น

  • PostgreSQL ผ่าน sqlx pool (internal/platform/db) โดยทุก repository ใช้ pool เดียวกัน
  • Redis ผ่าน named manager (internal/platform/redisx) โดย client ชื่อ redis ถูกใช้ทั้งงาน rate limit, cache และ OTP session
  • RabbitMQ ผ่าน internal/amqp (HA publisher, confirms, EnsureQueue) รวม 4 queue ได้แก่ line_change_richmenu, friend_track_event_trigger, booking_notification และ booking_event_trigger
  • Object storage (S3/MinIO) ผ่าน internal/storagex และ internal/s3x
  • LINE Platform ผ่าน internal/linehttp ดูรายละเอียดที่ การตรวจสิทธิ์ LIFF Token
  • ทุก feature ในหมวดนี้ถูก mount ผ่าน RegisterFunc ที่ระบุไว้ใน cmd/api/main.go
  • ฝั่ง client-web ที่เกี่ยวข้องโดยตรงคือ feature app-shell ซึ่งดูแล axios instance, base URL และการอ่าน error code จาก envelope