错误处理

捕获 listingsapi-js 错误类,读取平台错误码,并构建可识别 retryAfter 的退避机制。

SDK 抛出的所有错误都派生自 ListingsAPIError。无论平台以非 2xx HTTP 状态还是在 HTTP 200 响应体中报告 API 失败,SDK 都会抛出 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。SY10005,前提是 API 返回了错误代码。
errorsApiErrorEntry[]所有错误条目,均规范化为 { code, message, context }。
responseBodystring | null以字符串形式表示的原始响应体。
retryAfternumber | null仅适用于 RateLimitError。重试前需要等待的秒数,从 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 状态绝不意味着“请自行检查响应体”:

载荷中的错误标志抛出的错误类型
顶层 errors[],代码为 SY90005 或 SY90001AuthenticationError
顶层 errors[],代码为 SY90003PermissionDeniedError
顶层 errors[],代码为 RATE_LIMITEDRateLimitError
任何其他顶层 errors[]APIError
包含 errors[] 或 success: false 的写入结果ValidationError

对于写入操作(POST 和 DELETE 辅助函数),SDK 会检查响应中的每个结果对象;只要其中任何一个报告 success: false 或包含 errors[],SDK 就会抛出 ValidationError,并将 statusCode 设为 200。

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);}}

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) 方法会直接发送原始请求体,不执行这些检查。

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 实施超时控制,默认超时时间为 240000 毫秒,可通过构造函数选项 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);}}