오류 처리
listingsapi-js 오류 클래스를 처리하고, 플랫폼 오류 코드를 읽고, retryAfter을 고려한 백오프를 구현합니다.
SDK가 던지는 모든 오류는 ListingsAPIError를 상속합니다. API 실패 시에는 플랫폼이 2xx가 아닌 HTTP 상태로 보고하든 HTTP 200 본문 안에 보고하든 APIError 또는 그 하위 클래스의 오류를 던집니다. 네트워크 실패(DNS 오류, 연결 거부, 시간 초과) 시에는 APIConnectionError를 던집니다. success 플래그나 원시 페이로드를 확인할 필요가 없습니다. promise가 이행되면 호출이 성공한 것입니다.
오류 계층 구조
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모든 클래스는 패키지 루트에서 내보냅니다.
import {ListingsAPIError,APIError,AuthenticationError,PermissionDeniedError,NotFoundError,ValidationError,RateLimitError,InternalServerError,APIConnectionError,} from 'listingsapi-js';APIError 속성
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);}}타입별 오류 처리
인증 실패
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.');}}리소스 없음
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가 저장됩니다.
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');}}유효성 검사 실패
서버 측 유효성 검사 실패에는 플랫폼의 오류 항목이 포함됩니다.
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 상태라고 해서 "본문을 직접 확인해야 한다"는 의미는 아닙니다.
쓰기 작업(POST 및 DELETE 헬퍼)에서는 SDK가 응답의 모든 결과 객체를 검사합니다. 하나라도 success: false를 보고하거나 errors[]를 포함하면 statusCode가 200로 설정된 ValidationError를 던집니다.
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) 메서드는 이러한 검사 없이 원시 본문을 전송합니다.
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 안내 값을 우선 사용하고, 해당 값이 없으면 대기 시간을 지수적으로 늘리는 백오프 루프로 호출을 감쌉니다.
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 생성자 옵션으로 설정할 수 있습니다. 시간이 초과된 시도는 다른 연결 실패와 마찬가지로 재시도하며, 마지막 실패만 오류로 던집니다.
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);}}