오류 처리

listingsapi-js 오류 클래스를 처리하고, 플랫폼 오류 코드를 읽고, retryAfter을 고려한 백오프를 구현합니다.

SDK가 던지는 모든 오류는 ListingsAPIError를 상속합니다. API 실패 시에는 플랫폼이 2xx가 아닌 HTTP 상태로 보고하든 HTTP 200 본문 안에 보고하든 APIError 또는 그 하위 클래스의 오류를 던집니다. 네트워크 실패(DNS 오류, 연결 거부, 시간 초과) 시에는 APIConnectionError를 던집니다. success 플래그나 원시 페이로드를 확인할 필요가 없습니다. promise가 이행되면 호출이 성공한 것입니다.

오류 계층 구조

Code
ListingsAPIError                 base class for everything the SDK throws├── APIError                     any failure reported by the API│   ├── AuthenticationError      401, or invalid-token error payloads│   ├── PermissionDeniedError    403, or permission error payloads│   ├── NotFoundError            404, resource not found│   ├── ValidationError          400/422, mutations reporting success=false│   │                            or errors[], and client-side payload checks│   ├── RateLimitError           429, carries retryAfter│   └── InternalServerError      5xx, retry a read; read back a write first└── APIConnectionError           no response arrived: outcome unknown

모든 클래스는 패키지 루트에서 내보냅니다.

TypeScript
import {ListingsAPIError,APIError,AuthenticationError,PermissionDeniedError,NotFoundError,ValidationError,RateLimitError,InternalServerError,APIConnectionError,} from 'listingsapi-js';

APIError 속성

속성타입설명
statusCodenumber | null실패한 응답의 HTTP 상태는 401, 429, 500이며, 오류가 200 본문 안에 포함된 경우에는 200입니다. 클라이언트 측 유효성 검사 실패 시에는 null입니다.
codestring | null첫 번째 플랫폼 오류 코드, e.g. API가 오류 코드를 보낸 경우 SY10005입니다.
errorsApiErrorEntry[]{ code, message, context } 형식으로 정규화된 모든 오류 항목입니다.
responseBodystring | null문자열 형식의 원시 응답 본문입니다.
retryAfternumber | nullRateLimitError에만 해당합니다. 재시도 전에 대기할 시간(초)으로, Retry-After 헤더에서 파싱하며 헤더가 없으면 null입니다.
messagestring사람이 읽을 수 있는 요약으로, 오류 항목 또는 원시 본문이 뒤에 추가됩니다.
namestring구체적인 클래스 이름, e.g. "RateLimitError".
TypeScript
import { ListingsAPI, APIError, APIConnectionError } from 'listingsapi-js'; const client = new ListingsAPI(); try {await client.fetchAllLocations({ first: 10 });} catch (error) {if (error instanceof APIError) {  console.error(error.statusCode);   // 401, 429, 500, or 200 for payload errors  console.error(error.code);         // first platform code, e.g. 'SY10005'  console.error(error.errors);       // [{ code, message, context }, ...]  console.error(error.responseBody); // raw body text} else if (error instanceof APIConnectionError) {  // No response arrived. A read is safe to retry; for a write, read the  // record back before repeating it.  console.error('No response from the API:', error.message);}}

타입별 오류 처리

인증 실패

TypeScript
import { AuthenticationError } from 'listingsapi-js'; try {await client.fetchAllLocations({ first: 1 });} catch (error) {if (error instanceof AuthenticationError) {  console.error('Invalid API key. Rotate it under API Keys in the dashboard.');}}

리소스 없음

TypeScript
import { NotFoundError } from 'listingsapi-js'; try {await client.fetchPremiumListings(99999999);} catch (error) {if (error instanceof NotFoundError) {  console.error('Location not found.');}}

요청 빈도 제한

RateLimitError.retryAfter에는 서버의 Retry-After 값이 초 단위로 저장되며, 헤더가 없으면 null가 저장됩니다.

TypeScript
import { RateLimitError } from 'listingsapi-js'; try {await client.fetchAllLocations({ first: 100 });} catch (error) {if (error instanceof RateLimitError) {  const waitSeconds = error.retryAfter ?? 5;  console.error('Rate limited, retry in ' + waitSeconds + 's');}}

유효성 검사 실패

서버 측 유효성 검사 실패에는 플랫폼의 오류 항목이 포함됩니다.

TypeScript
import { ValidationError } from 'listingsapi-js'; try {await client.updateLocation({ id: 16808, phone: 'not-a-phone-number' });} catch (error) {if (error instanceof ValidationError) {  for (const entry of error.errors) {    console.error((entry.code ?? 'unknown') + ': ' + entry.message);  }}}

HTTP 200 응답 내부의 오류

플랫폼은 일부 실패를 HTTP 200과 함께 본문에 오류 페이로드를 포함하여 보고합니다. SDK는 모든 응답을 검사하고 이러한 경우에도 오류를 던지므로, 200 상태라고 해서 "본문을 직접 확인해야 한다"는 의미는 아닙니다.

페이로드의 오류 표시던지는 오류
코드가 SY90005 또는 SY90001인 최상위 errors[]AuthenticationError
코드가 SY90003인 최상위 errors[]PermissionDeniedError
코드가 RATE_LIMITED인 최상위 errors[]RateLimitError
그 밖의 모든 최상위 errors[]APIError
errors[] 또는 success: false이 있는 쓰기 결과ValidationError

쓰기 작업(POST 및 DELETE 헬퍼)에서는 SDK가 응답의 모든 결과 객체를 검사합니다. 하나라도 success: false를 보고하거나 errors[]를 포함하면 statusCode가 200로 설정된 ValidationError를 던집니다.

TypeScript
import { ValidationError } from 'listingsapi-js'; try {const result = await client.archiveLocations([16808]);console.log('Archived:', result);} catch (error) {if (error instanceof ValidationError) {  // The API answered 200 but rejected the mutation  console.error(error.statusCode); // 200  console.error(error.message);}}

게시물 헬퍼의 클라이언트 측 유효성 검사

게시물 헬퍼(createAnnouncement, createEvent, createOffer, bulkPublish)는 로컬에서 페이로드의 유효성을 검사하고 네트워크 호출 전에 ValidationError를 던집니다.

  • name는 공백만으로 이루어져서는 안 되며, locationIds는 비어 있어서는 안 됩니다.
  • sites는 GOOGLE 및 FACEBOOK로 구성된 집합의 비어 있지 않은 부분집합이어야 합니다.
  • message는 필수이며, 사이트별 맵인 경우 모든 대상 사이트에 공백만으로 이루어지지 않은 항목이 있어야 합니다.
  • ctaType와 ctaUrl는 함께 제공해야 하며, ctaType는 BOOK, ORDER, SHOP, LEARN_MORE, SIGN_UP, GET_OFFER 중 하나여야 합니다.
  • createEvent와 createOffer에는 추가로 title가 필요합니다.
  • bulkPublish의 postType는 ANNOUNCEMENT, EVENT, OFFER, COVID19, PRODUCT 중 하나여야 합니다.

이러한 로컬 오류에는 statusCode: null가 있어 서버 측 거부와 구분할 수 있습니다. 저수준 createPost(body) 메서드는 이러한 검사 없이 원시 본문을 전송합니다.

TypeScript
import { ListingsAPI, ValidationError } from 'listingsapi-js'; const client = new ListingsAPI(); try {await client.createAnnouncement({  name: 'Grand opening',  locationIds: [16808],  message: 'Acme Dental is now open in Midtown!',  ctaType: 'BOOK', // ctaUrl missing: throws before any request is sent});} catch (error) {if (error instanceof ValidationError && error.statusCode === null) {  console.error(error.message);  // Invalid post: ctaType and ctaUrl must be provided together}}

내장 재시도

클라이언트 자체에서 429 및 5xx 응답(Retry-After 헤더 준수)과 네트워크 실패에 대해 기본적으로 2회 재시도합니다. 이러한 시도가 모두 소진된 후에만 오류가 코드로 전달됩니다. maxRetries 및 timeout 옵션은 구성을 참조합니다.

retryAfter를 고려한 백오프 구현 예제

클라이언트의 내장 재시도가 끝난 후에도 계속 실행되어야 하는 장기 실행 작업에서는 서버의 retryAfter 안내 값을 우선 사용하고, 해당 값이 없으면 대기 시간을 지수적으로 늘리는 백오프 루프로 호출을 감쌉니다.

backoff.ts
import {ListingsAPI,RateLimitError,InternalServerError,APIConnectionError,} from 'listingsapi-js'; const client = new ListingsAPI(); async function withBackoff<T>(fn: () => Promise<T>,maxAttempts = 5,): Promise<T> {for (let attempt = 1; attempt <= maxAttempts; attempt++) {  try {    return await fn();  } catch (err) {    const retryable =      err instanceof RateLimitError ||      err instanceof InternalServerError ||      err instanceof APIConnectionError;    if (!retryable || attempt === maxAttempts) throw err;     let delayMs = 500 * 2 ** (attempt - 1);    if (err instanceof RateLimitError && err.retryAfter !== null) {      delayMs = err.retryAfter * 1000;    }    await new Promise((resolve) => setTimeout(resolve, delayMs));  }}throw new Error('unreachable');} const page = await withBackoff(() => client.fetchAllLocations({ first: 25 }));

NotFoundError, ValidationError, AuthenticationError, PermissionDeniedError는 의도적으로 재시도하지 않습니다. 입력이나 키를 변경하지 않고 같은 요청을 반복해도 성공할 수 없기 때문입니다.

네트워크 실패 및 시간 초과

응답이 도착하지 못하게 하는 모든 상황(DNS 실패, 연결 거부 또는 요청별 제한 시간 초과)에서는 APIConnectionError를 던집니다. SDK는 fetch의 네이티브 TypeError 또는 AbortError를 외부로 그대로 전달하지 않습니다.

APIConnectionError는 요청이 서버에 도달하지 않았다는 의미가 아닙니다. DNS 실패나 연결 거부 시에는 API에 도달하기 전에 중단되지만, 시간 초과도 같은 클래스를 사용하며 클라이언트가 대기를 중단한 후에 요청이 도착하여 완료되었을 수 있습니다. 이를 결과를 알 수 없는 상태로 취급합니다. 읽기는 자유롭게 재시도할 수 있지만, API가 반복된 쓰기를 감지하지 않으므로 쓰기는 반복하기 전에 해당 레코드를 다시 읽어 확인합니다.

각 시도에는 내부적으로 AbortSignal.timeout를 사용하여 제한 시간을 적용하며, 기본값은 240000ms이고 timeout 생성자 옵션으로 설정할 수 있습니다. 시간이 초과된 시도는 다른 연결 실패와 마찬가지로 재시도하며, 마지막 실패만 오류로 던집니다.

TypeScript
import { ListingsAPI, APIConnectionError } from 'listingsapi-js'; const client = new ListingsAPI({ timeout: 15000 }); try {await client.fetchAllLocations({ first: 10 });} catch (error) {if (error instanceof APIConnectionError) {  console.error('Network failure or timeout:', error.message);}}
  • 구성, timeout 및 maxRetries 옵션
  • 빠른 시작, 최소한의 오류 처리 예제
  • 사용 사례, 완전한 스크립트에서 실제로 사용하는 오류 처리