오류
Sursa는 요청의 성공 여부를 HTTP 상태 코드로 알리고, 실패한 이유를 바뀌지 않는 오류 코드로 알려요. 모든 오류 응답은 형태가 같아요.
{
"detail": "the cursor is malformed or was issued for different filters",
"error": {
"code": "api.cursor_invalid",
"message": "the cursor is malformed or was issued for different filters",
"details": {"parameter": "cursor"},
"request_id": "01JB7Q2M4X9V3K8T5N6P0R1S2A",
"doc_url": "https://developers.sursa.com/errors/api.cursor_invalid"
}
}
| 필드 | 의미 |
|---|---|
error.code | <area>.<reason> 형식의 고정된 코드. 처리 분기는 이 값을 기준으로 하세요. |
error.message | 사람이 읽을 수 있는 영어 설명. 문구가 언제든 바뀔 수 있으니 파싱하지 마세요. |
error.details | 파라미터, 한도, 재시도 시점처럼 오류에 관한 구조화된 정보. 필드는 코드에 따라 달라요. |
error.request_id | 이 요청의 ID. X-Request-Id 응답 헤더 값과 같아요. |
error.doc_url | 이 포털에 있는 해당 코드의 페이지. |
detail | 기존 연동을 위해 남겨 둔 이전 형식의 오류. 새 코드에서는 error 값을 읽으세요. |
모든 코드에는 오류 코드 아래에 HTTP 상태, details 필드, 그 코드를 반환하는 엔드포인트를 담은 페이지가
있어요. API 레퍼런스의 각 엔드포인트 페이지에는 그 엔드포인트가 반환할 수 있는 코드가 나와 있어요.
HTTP 상태
| 상태 | 의미 |
|---|---|
200, 201, 202, 204 | 요청이 성공했어요. 202는 요청을 받아들였고 아직 처리 중이라는 뜻이에요. |
400 | 요청 형식이 잘못되었어요. 파라미터, 커서, metadata, Idempotency-Key 중 잘못된 값이 있어요. 요청을 고치세요. |
401 | 유효한 키가 없어요. 인증을 참고하세요. |
402 | 카드 수나 저장 용량처럼 조직 요금제의 한도에 도달했어요. |
403 | 키는 유효하지만 이 요청을 수행할 수 없어요. 역할, 스코프, 요금제 기능 중 하나가 없어요. |
404 | 객체가 없거나, 키가 그 객체를 볼 수 없어요. |
409 | 요청이 현재 상태와 충돌해요. 중복된 external_id, 열려 있는 동기화, 같은 Idempotency-Key로 아직 실행 중인 요청 등이 원인이에요. |
410 | 객체가 만료되었거나(예: 24시간이 지난 업로드) 경로가 지원 종료되었어요. |
413 | 본문이나 파일이 너무 커요. |
415 | 지원하지 않는 파일 형식이거나, 파일 내용이 이름과 맞지 않아요. |
422 | 요청 형식은 올바르지만 내용이 유효하지 않아요. 필드가 빠졌거나 값이 범위를 벗어났어요. |
429 | 요청이 너무 많거나 할당량을 다 썼어요. 호출 한도와 할당량을 참고하세요. |
500, 502, 503, 504 | Sursa 쪽의 문제예요. 나중에 다시 시도하세요. |
유효성 검사 오류
validation.failed 코드의 422 응답에서는 detail 값이 유효성 검사를 통과하지 못한
모든 필드를 위치와 함께 나열해요.
{
"detail": [
{"loc": ["body", "title"], "msg": "String should have at least 1 character", "type": "string_too_short"}
],
"error": {"code": "validation.failed", "message": "request validation failed", "request_id": "01JB7…"}
}
재시도할 오류
| 오류 | 재시도 여부 |
|---|---|
| 네트워크 오류나 타임아웃으로 응답이 없음 | 예. 같은 Idempotency-Key로 재시도하세요. |
429 | 예. Retry-After에 적힌 초만큼 기다린 뒤 재시도하세요. |
500, 502, 503, 504 | 예. 지수 백오프로 재시도하세요. 1초, 2초, 4초… 최대 1분까지 늘리고, 무작위 지터를 더하세요. |
409 api.idempotency_in_progress | 예. Retry-After 이후에 재시도하세요. 이 키로 보낸 첫 요청이 아직 실행 중이에요. |
그 밖의 모든 4xx | 아니요. 같은 요청은 다시 실패해요. 먼저 요청을 고치세요. |
POST와 PATCH 요청은 Idempotency-Key를 붙였을 때만 재시도하세요. 그래야 응답을
받지 못해 재시도하더라도 객체가 두 번 생기지 않아요. GET, PUT, DELETE는 그대로 재시도해도 안전해요.
// Retries a request on network errors, 429 and 5xx. The same Idempotency-Key is sent on every attempt.
async function withRetries(send, attempts = 5) {
for (let attempt = 1; ; attempt++) {
let response;
try {
response = await send();
} catch (error) {
if (attempt >= attempts) throw error;
}
if (response && response.status !== 429 && response.status < 500) return response;
if (attempt >= attempts) return response;
const retryAfter = Number(response?.headers.get("Retry-After"));
const backoff = Math.min(60, 2 ** (attempt - 1)) * (0.8 + Math.random() * 0.4);
await new Promise((resolve) => setTimeout(resolve, 1000 * (retryAfter || backoff)));
}
}
const key = crypto.randomUUID();
const response = await withRetries(() =>
fetch("https://api.sursa.com/v1/cards/text", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SURSA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify({ title: "Refund policy", body_markdown: "Refunds take 14 days." }),
}),
);문제 알리기
성공 여부와 관계없이 모든 응답에는 X-Request-Id 헤더가 있고, 모든 오류의 error.request_id에 같은 값이 들어 있어요.
이 값을 직접 남기는 요청 기록과 함께 로그에 저장하세요. 특정 요청에 대해 지원팀에 문의할 때 요청 ID를 보내 주시면 그
요청의 기록을 바로 찾을 수 있어요.
모르는 코드 처리하기
새 오류 코드는 API의 어느 버전에서든 추가될 수 있어요. 모르는 코드는 HTTP 상태로 판단하세요. 모르는 4xx는 그대로는
성공하지 않을 요청으로, 모르는 5xx는 일시적인 문제로 처리하세요.