{"record":{"id":"6dfc59145ed120a5","repo":"CherryHQ/cherry-studio","slug":"ppio-api-error-response-status-errortext","errorCode":null,"errorMessage":"PPIO API error: ${response.status} - ${errorText}","messagePattern":"PPIO API error: (.+?) - (.+?)","errorType":"exception","errorClass":"PpioApiError","httpStatus":null,"severity":"error","filePath":"src/main/ai/provider/custom/ppio/ppioTransport.ts","lineNumber":159,"sourceCode":"    const fetchOptions: RequestInit = {\n      method,\n      headers: {\n        'Content-Type': 'application/json',\n        Authorization: `Bearer ${this.apiKey}`\n      },\n      signal: controller.signal\n    }\n\n    if (method === 'POST') {\n      fetchOptions.body = JSON.stringify(body)\n    }\n\n    try {\n      const response = await fetch(url, fetchOptions)\n\n      if (!response.ok) {\n        const errorText = (await response.text().catch(() => '')).slice(0, 500)\n        throw new PpioApiError(`PPIO API error: ${response.status} - ${errorText}`, response.status)\n      }\n\n      const data = await response.json()\n      return data as T\n    } catch (error) {\n      if (error instanceof Error && error.name === 'AbortError') {\n        if (externallyAborted) {\n          throw createAbortError('PPIO API request aborted')\n        }\n\n        throw new Error(`PPIO API request timeout after ${timeout / 1000}s`)\n      }\n      throw error\n    } finally {\n      clearTimeout(timeoutId)\n      externalSignal?.removeEventListener('abort', onExternalAbort)\n    }\n  }","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/provider/custom/ppio/ppioTransport.ts#L141-L177","documentation":"PpioApiError is thrown by PpioTransport.request() for any non-ok HTTP response from api.ppio.com (submit, poll, or getTaskResult). It is a custom Error subclass carrying `error.statusCode` so the poll loop can decide terminal (4xx≠429) vs transient (5xx/429). The message embeds the status plus the first 500 chars of the body.","triggerScenarios":"Any PPIO endpoint returns 4xx/5xx. Concrete causes: missing/invalid apiKey (401/403), quota/credit exhausted (402/429), unknown model id (400/404), NSFW content rejected (400 with policy reason), malformed request (422), or 5xx during vendor outage.","commonSituations":"First run with unset apiKey, credits run out mid-batch, model id drift after PPIO renames models, content policy trip on a prompt, or transient 5xx during peak load.","solutions":["Inspect error.statusCode and the embedded errorText: 401/403 → fix apiKey; 402/429 → top up credits / back off; 400 with policy → adjust prompt; 400 model-not-found → correct model id.","For 5xx or 429, retry (the poll loop already does up to maxTransientRetries; for submit, add an outer retry with backoff).","Verify the model id is still listed in PPIO's catalog and matches a PpioModelDescriptor.","Confirm baseURL is the PPIO API host (default https://api.ppio.com) and the endpoint path matches the model's descriptor.endpoint."],"exampleFix":"// before\ntry { await transport.submit(input) } catch (e) { /* opaque */ }\n// after — classify by status\ntry {\n  await transport.submit(input)\n} catch (e) {\n  if (e instanceof PpioApiError && isTerminalHttpStatus(e.statusCode)) throw e\n  await backoffRetry(() => transport.submit(input))\n}","handlingStrategy":"try-catch","validationCode":"if (!settings.apiKey) throw new Error('PPIO apiKey is required')\nif (!/^https?:\\/\\//.test(baseURL)) throw new Error(`Invalid PPIO baseURL: ${baseURL}`)","typeGuard":"export function isPpioApiError(e: unknown): e is { statusCode: number; message: string } {\n  return e instanceof Error && e.name === 'PpioApiError' && typeof (e as any).statusCode === 'number'\n}","tryCatchPattern":"try {\n  await transport.submit(input)\n} catch (e) {\n  if (isPpioApiError(e) && isTerminalHttpStatus(e.statusCode)) throw e // 4xx≠429\n  await backoffRetry(() => transport.submit(input)) // 5xx / 429\n}","preventionTips":["Validate apiKey and baseURL before constructing the transport.","Key off error.statusCode to separate terminal from transient.","Inspect the embedded errorText — it usually pinpoints the bad field."],"tags":["network","api","ppio","http","image-generation"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}