# Lỗi

Mọi API của Hạ Tầng, từ Email, Thanh toán tới chat gateway, báo lỗi theo cùng một dạng JSON kèm mã HTTP theo quy ước. Trang này giải thích dạng lỗi đó, liệt kê các lỗi bạn dễ gặp nhất cùng cách xử lý, và cách bắt lỗi trong code. [Tham chiếu lỗi](https://hatang.dev/vi/docs/reference/errors.md) liệt kê mọi mã lỗi.

## Dạng lỗi

Request thất bại trả status `4xx` hoặc `5xx` và body như sau:

```json
{
  "statusCode": 422,
  "name": "validation_error",
  "message": "to: Invalid email address"
}
```

| Trường | Kiểu | Ý nghĩa |
|---|---|---|
| `statusCode` | number | Mã HTTP, lặp lại trong body |
| `name` | string | Mã ổn định cho máy đọc. **Rẽ nhánh theo trường này.** |
| `message` | string | Một câu tiếng Anh cho người đọc và cho log. Có thể đổi bất cứ lúc nào; đừng phân tích nội dung của nó. |

Với lỗi kiểm tra dữ liệu, `message` nêu trường đầu tiên bị sai, theo dạng `trường: vấn đề`.

## Mã trạng thái

| Status | Ý nghĩa | Thử lại? |
|---|---|---|
| `400` | Request xung đột với một request trước đó (idempotency) | Không, sửa request |
| `401` | Thiếu API key, hoặc key không được phép làm việc này | Không |
| `403` | Key không hợp lệ, module đang tắt, hoặc thao tác không được phép ở trạng thái này | Không |
| `404` | Tài nguyên không tồn tại, hoặc thuộc doanh nghiệp hay môi trường khác | Không |
| `409` | Xung đột với trạng thái hiện tại (trùng lặp, hội thoại đã đóng, request đồng thời) | Đôi khi, sau khi đọc lại trạng thái |
| `413` | Tệp tải lên quá lớn | Không |
| `422` | Body hoặc query không hợp lệ | Không, sửa request |
| `429` | Chạm giới hạn tần suất hoặc hạn mức trong ngày | Có, sau khi chờ (chỉ với giới hạn tần suất) |
| `500`, `503` | Có lỗi phía Hạ Tầng hoặc phía nhà cung cấp | Có, với backoff |

## Các lỗi thường gặp

| `name` | Status | Ý nghĩa | Nên làm gì |
|---|---|---|---|
| `validation_error` | 422 | Thiếu trường, trường sai, hoặc body không phải JSON hợp lệ | Đọc `message` và sửa request. Đừng gửi lại nguyên như cũ. |
| `validation_error` | 403 | Chỉ ở Email: tên miền của `from` chưa xác minh | [Xác minh tên miền](https://hatang.dev/vi/docs/guides/email/domains.md) |
| `missing_api_key` | 401 | Thiếu header `Authorization: Bearer` | Gửi kèm key |
| `invalid_api_key` | 403 | Key sai định dạng, không tồn tại hoặc đã bị thu hồi | Kiểm tra key; tạo key mới nếu key đã bị thu hồi |
| `restricted_api_key` | 401 | Key thiếu scope, hoặc giới hạn tên miền, origin của key không cho phép | Dùng key có đúng [scope](https://hatang.dev/vi/docs/guides/getting-started/authentication.md#scopes) |
| `product_disabled` | 403 | Module đang tắt với doanh nghiệp | Nhờ chủ sở hữu hoặc quản trị viên bật lại; đừng thử lại |
| `environment_mismatch` | 403 | Thao tác hoặc key Thanh toán không khớp với môi trường | Kiểm tra `Hatang-Environment` và key |
| `not_found` | 404 | Doanh nghiệp (và môi trường) này không có tài nguyên đó | Kiểm tra id; có thể nó thuộc doanh nghiệp hay môi trường khác |
| `invalid_idempotent_request` | 400 | `Idempotency-Key` đã được dùng với body khác | Dùng key mới cho request mới ([Idempotency](https://hatang.dev/vi/docs/guides/getting-started/idempotency.md)) |
| `concurrent_idempotent_requests` | 409 | Hai request cùng `Idempotency-Key` chạy cùng lúc | Chờ một chút rồi thử lại; bạn nhận kết quả của request đầu |
| `rate_limit_exceeded` | 429 | Quá nhiều request mỗi giây cho module này | Chờ `retry-after` giây rồi thử lại ([Giới hạn tần suất](https://hatang.dev/vi/docs/guides/getting-started/rate-limits.md)) |
| `daily_quota_exceeded` | 429 | Doanh nghiệp đã dùng hết hạn mức email trong ngày | Chờ đến 0 giờ UTC hoặc nâng hạn mức |
| `conversation_closed` | 409 | Hội thoại chat đã đóng | Mở lại, hoặc bắt đầu hội thoại mới |
| `file_too_large`, `unsupported_file_type` | 413, 422 | Tệp tải lên quá lớn hoặc thuộc loại không được nhận | Xem [giới hạn](https://hatang.dev/vi/docs/reference/limits.md) |
| `invalid_state` | 409 | Đối tượng không làm được việc đó lúc này, như huỷ một thanh toán đã trả | Lấy lại đối tượng và kiểm tra trạng thái |
| `application_error` | 500 hoặc 403 | Lỗi ngoài dự kiến; trả `403` khi việc gửi email của doanh nghiệp đang tạm dừng | Thử lại `500` với backoff; với `403`, mở lại việc gửi ở **Email → Cài đặt email** |
| `internal_server_error` | 500 | Có lỗi phía Hạ Tầng | Thử lại với backoff; liên hệ hỗ trợ nếu lỗi kéo dài |
| `provider_unavailable` | 503 | Ngân hàng hoặc nhà cung cấp hoá đơn điện tử không phản hồi | Thử lại sau |

## Xử lý lỗi trong code

Quyết định theo `name` và `statusCode`: thử lại lỗi tạm thời, báo cho người phụ trách lỗi cần người sửa, và ghi log phần còn lại cùng request.

**Node.js**

SDK Node.js không bao giờ throw khi API báo lỗi. Mọi hàm trả về `{ data, error }`, trong đó `error` có cùng `statusCode`, `name` và `message`. Khi hoàn toàn không kết nối được tới API, `error.statusCode` là `0` và `error.name` là `application_error`.

```js
import { Hatang } from "@hatang/sdk";

const hatang = new Hatang(process.env.HATANG_API_KEY, { baseUrl: "https://api.hatang.dev" });

const { data, error } = await hatang.emails.send({ from, to, subject, html }, { idempotencyKey: orderId });

if (error) {
  switch (error.name) {
    case "validation_error":
      // Lỗi của code hoặc dữ liệu đầu vào: ghi log và sửa payload.
      logger.error({ error }, "Hatang từ chối email");
      break;
    case "rate_limit_exceeded":
    case "internal_server_error":
    case "application_error":
      // Tạm thời: thử lại sau với cùng idempotency key.
      await queue.retryLater({ orderId });
      break;
    case "product_disabled":
    case "restricted_api_key":
    case "invalid_api_key":
      // Cấu hình: báo cho người phụ trách, đừng thử lại.
      await alertOps(error.message);
      break;
    default:
      logger.error({ error }, "Lỗi Hatang ngoài dự kiến");
  }
}
```

**Python**

```python
from hatang import Hatang, HatangError

hatang = Hatang()  # đọc HATANG_API_KEY

try:
    hatang.emails.send(payload, idempotency_key=order_id)
except HatangError as err:
    if err.retryable:                    # bị giới hạn, lỗi 5xx hoặc không tới được (status_code 0)
        retry_later(order_id)            # tạm thời
    elif err.name in ("product_disabled", "restricted_api_key", "invalid_api_key"):
        alert_ops(err.message)           # cấu hình
    else:
        raise ValueError(f"{err.name}: {err.message}")  # sửa request
```

**PHP**

```php
$res = $client->post('/emails', [
    'headers' => ['Idempotency-Key' => $orderId],
    'json' => $payload,
    'http_errors' => false,
]);

if ($res->getStatusCode() >= 400) {
    $err = json_decode((string) $res->getBody(), true);
    switch ($err['name'] ?? '') {
        case 'rate_limit_exceeded':
            retryLater($orderId);
            break;
        case 'product_disabled':
        case 'restricted_api_key':
        case 'invalid_api_key':
            alertOps($err['message']);
            break;
        default:
            if ($res->getStatusCode() >= 500) {
                retryLater($orderId);
            } else {
                throw new RuntimeException($err['name'] . ': ' . $err['message']);
            }
    }
}
```

> **Tip: Tìm các lời gọi lỗi**
>
> **Email → Logs** trong dashboard liệt kê mọi request API của doanh nghiệp kèm status, thời gian xử lý, body của request và response. Xem các lời gọi có status `4xx` hoặc `5xx` để biết chính xác lỗi mỗi lời gọi nhận về.

## Bước tiếp theo

- [Tham chiếu lỗi](https://hatang.dev/vi/docs/reference/errors.md): mọi mã lỗi
- [Giới hạn tần suất](https://hatang.dev/vi/docs/guides/getting-started/rate-limits.md)
- [Idempotency](https://hatang.dev/vi/docs/guides/getting-started/idempotency.md)

Source: https://hatang.dev/vi/docs/guides/getting-started/errors
