18. Xử lý lỗi & Rate limit

Phân loại exception của SDK, đọc mã lỗi server, retry an toàn khi bị rate limit và không gửi lại lệnh bị trùng

Mục tiêu

Biết bắt đúng exception của SDK và phản ứng đúng với từng loại: lỗi xác thực, lỗi nhập liệu bị chặn tại client, lỗi server kèm mã lỗi, rate limit và lệnh bị trùng (HTTP 409). Tương ứng Sample 18 trong repo tutorials (SDK v3.3.2).

Phần A và B chạy offline bằng exception giả (không gọi mạng). Chỉ phần C gọi API thật, và chỉ đọc dữ liệu, không đặt lệnh. Cần OTP cho phần C.

Phân cấp exception

Bắt lớp con trước, lớp cha sau.

SSIError                      message, code, status_code, response_body, headers
 ├─ AuthenticationError
 │    ├─ SmartOTPPendingError     chưa bấm APPROVE trên app (HTTP 202 / 401114)
 │    ├─ SmartOTPRejectedError    bị từ chối / hết hạn / transaction lạ -> xin OTP mới
 │    └─ ReauthenticationRequired không refresh được phiên -> xác thực lại bằng OTP mới
 ├─ APIError                     code = mã lỗi server (vd "400210") hoặc status HTTP dạng chuỗi
 │    └─ DuplicateRequestError    HTTP 409, clientRequestId/batchRequestId đã dùng trong ngày
 ├─ RateLimitError               retry_after_ms (mili-giây), limit, remaining
 ├─ ValidationError              dữ liệu sai, bị chặn ngay phía client (chưa gửi request)
 └─ WebSocketError
      └─ TradingWSError          status, error_code, server_message

Các mẫu trong repo

Ngôn ngữFile
Pythonpython/sample_18_error_handling.py, python/sample_18_error_handling_async.py
Node.jsnode/sample_18_error_handling.js
.NETdotnet/Sample18ErrorHandling.cs
Gogo/sample_18_error_handling.go

Trang này minh họa bằng Python. Tên exception ở các ngôn ngữ khác khác nhau đôi chút (ví dụ .NET dùng SsiException, RateLimitException); xem Exceptions của từng SDK. Repo: ssi-fastconnect-v3-tutorials.

Chuẩn bị

  • Cấu hình credentials trong config.json, xem Tutorial 1. Sample repo đọc cấu hình qua config.py; ở đây dùng placeholder để code gọn.
  • auth_helper.py (token cache, Tutorial 1) và OTP cho lần đăng nhập đầu (chỉ cần cho phần C).

Sample Code — Sync

Code dưới đây rút gọn từ sample trong repo, giữ nguyên luồng.

python/sample_18_error_handling.py
import dataclasses
import time

from ssi_sdk import Auth, Data, Config
from ssi_sdk.enums import ServerErrorCode
from ssi_sdk.exceptions import (
    APIError,
    AuthenticationError,
    DuplicateRequestError,
    RateLimitError,
    ReauthenticationRequired,
    SmartOTPPendingError,
    SmartOTPRejectedError,
    SSIError,
    ValidationError,
)
from auth_helper import ensure_auth

config = Config(
    client_id="<your_client_id>",
    api_key="<your_api_key>",
    api_secret="<your_api_secret>",
    private_key="<your_private_key>",
)
MAX_ATTEMPTS = 3
MAX_WAIT_MS = 10_000


def explain(e: Exception) -> str:
    """Trả về hướng xử lý cho một exception. Thứ tự except: con trước, cha sau."""
    try:
        raise e
    except RateLimitError as err:
        return (f"Chạm rate limit (limit={err.limit}, remaining={err.remaining}). "
                f"Đợi {err.retry_after_ms} ms rồi gọi lại (chỉ với request đọc).")
    except DuplicateRequestError as err:  # phải đứng TRƯỚC APIError
        return f"409 trùng id ({err.message}). Lệnh gốc có thể đã nằm trên sàn: tra sổ lệnh, KHÔNG gửi lại."
    except ValidationError as err:
        return f"Dữ liệu không hợp lệ, chưa gửi request: {err.message}. Sửa input rồi chạy lại."
    except APIError as err:
        known = ServerErrorCode.from_value(err.code)
        name = known.name if known else "mã chưa có trong SDK"
        return f"Server từ chối: code={err.code} ({name}) status={err.status_code}: {err.message}"
    except SmartOTPPendingError:  # các lớp con của AuthenticationError đứng TRƯỚC lớp cha
        return "Chưa bấm APPROVE trên app SSI iBoard: bấm APPROVE rồi chờ tiếp, hoặc tăng số lần poll."
    except SmartOTPRejectedError as err:
        return f"Smart OTP bị từ chối/hết hạn ({err.message}): yêu cầu OTP mới."
    except ReauthenticationRequired:
        return "Không refresh được phiên: xác thực lại bằng OTP mới."
    except AuthenticationError as err:
        return f"Lỗi xác thực: {err.message}"
    except SSIError as err:
        return f"Lỗi SDK khác ({type(err).__name__}): {err.message}"


def call_with_retry(fn, *args, **kwargs):
    """Gọi request ĐỌC, tôn trọng retry_after_ms khi bị rate limit. KHÔNG dùng cho đặt/hủy lệnh."""
    for attempt in range(1, MAX_ATTEMPTS + 1):
        try:
            return fn(*args, **kwargs)
        except RateLimitError as e:
            if attempt == MAX_ATTEMPTS:
                raise
            wait_ms = min(e.retry_after_ms or config.retry_delay_ms, MAX_WAIT_MS)
            print(f"  Rate limit, đợi {wait_ms:.0f} ms (lần {attempt}/{MAX_ATTEMPTS})...")
            time.sleep(wait_ms / 1000)


# Phần A (offline): cấu hình theo mili-giây
print("--- A. Cấu hình theo mili-giây ---")
print(f"timeout_ms={config.timeout_ms} retry_delay_ms={config.retry_delay_ms} "
      f"otp_poll_interval_ms={config.otp_poll_interval_ms} otp_poll_max_wait_ms={config.otp_poll_max_wait_ms}")
try:
    dataclasses.replace(config, timeout=5)  # tên cũ không còn
except TypeError as e:
    print(f"Tên cũ 'timeout' bị từ chối đúng như mong đợi: {e}")

# Phần B (offline): từng loại lỗi bằng exception giả
print("\n--- B. Hướng xử lý từng loại lỗi (exception giả, không gọi mạng) ---")
samples = [
    RateLimitError("Too many requests", retry_after_ms=1500, limit=10, remaining=0),
    ValidationError("price phải > 0"),
    DuplicateRequestError("clientRequestId đã dùng", code="409", status_code=409),
    APIError("Timeframe không hỗ trợ", code="400210", status_code=400),
    APIError("Mã lỗi lạ", code="999999", status_code=400),
    SmartOTPPendingError("chưa approve"),
    SmartOTPRejectedError("transaction hết hạn"),
    ReauthenticationRequired("refresh token bị thu hồi"),
]
for exc in samples:
    print(f"- {type(exc).__name__}: {explain(exc)}")

# Phần C (thật, chỉ đọc): xác thực, gọi API, bắt lỗi
print("\n--- C. Gọi API thật (chỉ đọc) ---")
with Auth(config) as auth:
    try:
        ensure_auth(auth, otp="<your_otp>")
    except (SmartOTPPendingError, SmartOTPRejectedError, ReauthenticationRequired, AuthenticationError) as e:
        raise SystemExit(f"Xác thực thất bại: {explain(e)}")

    with Data(auth) as data:
        try:
            rows = call_with_retry(data.market_data.get_master_data)
            print(f"get_master_data: {len(rows)} mã")
        except SSIError as e:
            print(f"get_master_data lỗi: {explain(e)}")

        # Nến tuần/tháng không được hỗ trợ: ValidationError ngay phía client
        try:
            data.market_data.get_ohlc_1week_historical("SSI", "2026/01/01 00:00:00", "2026/03/27 23:59:59")
        except SSIError as e:
            print(f"get_ohlc_1week_historical: {explain(e)}")

Sample Code — Async

Async dùng cùng luồng; chỉ khác AsyncAuth / AsyncData, await và asyncio.sleep thay cho time.sleep trong hàm retry.

python/sample_18_error_handling_async.py
import asyncio

from ssi_sdk import AsyncAuth, AsyncData, Config
from ssi_sdk.exceptions import RateLimitError, SSIError
from auth_helper import ensure_auth_async

config = Config(...)
MAX_ATTEMPTS = 3
MAX_WAIT_MS = 10_000

# explain(e) giống bản sync


async def call_with_retry(fn, *args, **kwargs):
    """Gọi request ĐỌC, tôn trọng retry_after_ms khi bị rate limit. KHÔNG dùng cho đặt/hủy lệnh."""
    for attempt in range(1, MAX_ATTEMPTS + 1):
        try:
            return await fn(*args, **kwargs)
        except RateLimitError as e:
            if attempt == MAX_ATTEMPTS:
                raise
            wait_ms = min(e.retry_after_ms or config.retry_delay_ms, MAX_WAIT_MS)
            await asyncio.sleep(wait_ms / 1000)


async def main():
    async with AsyncAuth(config) as auth:
        await ensure_auth_async(auth, otp="<your_otp>")

        async with AsyncData(auth) as data:
            try:
                rows = await call_with_retry(data.market_data.get_master_data)
                print(f"get_master_data: {len(rows)} mã")
            except SSIError as e:
                print(f"get_master_data lỗi: {explain(e)}")


asyncio.run(main())

Quy tắc quan trọng

Quy tắcGiải thích
Bắt con trước, cha sauDuplicateRequestError phải đứng trước APIError; SmartOTPPendingError, SmartOTPRejectedError, ReauthenticationRequired đứng trước AuthenticationError. Đảo thứ tự thì nhánh cụ thể không bao giờ chạy
Thời gian tính bằng mili-giâyMọi tham số thời gian ở SDK 3.3.x có hậu tố *_ms (timeout_ms, retry_delay_ms, otp_poll_interval_ms...). Tên cũ (timeout, retry_delay, poll_interval...) báo TypeError
Chỉ request GET được tự retryLệnh đặt/sửa/hủy (POST/PUT/DELETE) không bao giờ tự retry. Gặp lỗi mạng, hãy tra sổ lệnh trước, đừng gửi lại mù quáng
DuplicateRequestError (409)clientRequestId hoặc batchRequestId đã dùng trong ngày. Lệnh gốc có thể đang nằm trên sàn: tra cứu theo id, không gửi lại
So sánh mã lỗi qua enumAPIError.code có thể là chuỗi hoặc số. Dùng ServerErrorCode.from_value(code); trả None với mã lạ, không raise
Không in dữ liệu nhạy cảmKhông in e.headers, e.response_body hay config: có thể chứa token, số tài khoản. Chỉ in message, code, status_code

Lưu ý

  • Retry chỉ cho lỗi tạm thời: call_with_retry chỉ bắt RateLimitError, chờ retry_after_ms (có trần MAX_WAIT_MS) rồi gọi lại tối đa MAX_ATTEMPTS lần. Không dùng nó cho đặt/hủy lệnh. Nếu tự xây cơ chế retry cho lệnh, hãy giữ nguyên client_request_id giữa các lần gửi để không tạo lệnh thứ hai, và kiểm tra sổ lệnh trước khi gửi lại.
  • ValidationError không cần retry: dữ liệu sai bị chặn trước khi gửi request, sửa input rồi chạy lại. Ví dụ get_ohlc_1week_historical luôn báo ValidationError vì server không hỗ trợ nến 1w / 1M (mã lỗi 400210).
  • Mã lỗi server: tra cứu ý nghĩa ở Tra cứu mã lỗi (ErrCode) và ServerErrorCode trong Exceptions (Python). Một số mã được dùng lại cho nhiều ý nghĩa, nên hãy tin số mã hơn là text.
  • Lỗi trong batch là dữ liệu trong kết quả từng lệnh, không phải exception: xem Đặt/hủy lệnh hàng loạt.
  • Trading WebSocket ném TradingWSError (import từ ssi_sdk, kèm status, error_code, server_message): xem Trading WebSocket.
  • Chi tiết từng exception và ví dụ khác: Exceptions (Python).

Trên trang này