错误处理
捕获 listingsapi-js 错误类,读取平台错误码,并构建可识别 retryAfter 的退避机制。
SDK 抛出的所有错误都派生自 ListingsAPIError。无论平台以非 2xx HTTP 状态还是在 HTTP 200 响应体中报告 API 失败,SDK 都会抛出 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[],SDK 就会抛出 ValidationError,并将 statusCode 设为 200。
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);}}post 辅助函数中的客户端验证
post 辅助函数(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 实施超时控制,默认超时时间为 240000 毫秒,可通过构造函数选项 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);}}