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_messageCác mẫu trong repo
| Ngôn ngữ | File |
|---|---|
| Python | python/sample_18_error_handling.py, python/sample_18_error_handling_async.py |
| Node.js | node/sample_18_error_handling.js |
| .NET | dotnet/Sample18ErrorHandling.cs |
| Go | go/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 quaconfig.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.
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.
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ắc | Giải thích |
|---|---|
| Bắt con trước, cha sau | DuplicateRequestError 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ây | Mọ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ự retry | Lệ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 enum | APIError.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ảm | Khô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_retrychỉ bắtRateLimitError, chờretry_after_ms(có trầnMAX_WAIT_MS) rồi gọi lại tối đaMAX_ATTEMPTSlầ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ênclient_request_idgiữ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. ValidationErrorkhô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_historicalluôn báoValidationErrorvì server không hỗ trợ nến1w/1M(mã lỗi400210).- Mã lỗi server: tra cứu ý nghĩa ở Tra cứu mã lỗi (ErrCode) và
ServerErrorCodetrong 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èmstatus,error_code,server_message): xem Trading WebSocket. - Chi tiết từng exception và ví dụ khác: Exceptions (Python).