Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions __tests__/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,52 @@ describe("run output paths", () => {
});
});

describe("failed run traceability", () => {
// Every bug report filed through `anyapi report-bug` on 2026-08-26 said "no
// request id was produced": the gateway sends one on X-Anyapi-Request-Id for
// failures too, and this client dropped it, leaving support to reconstruct
// the run from a customer id and a timestamp.
it("surfaces the gateway request id on a failed run", async () => {
const fetchImpl: FetchLike = async () =>
new Response(
JSON.stringify({ error: "all providers failed", code: "all_providers_failed" }),
{
status: 502,
headers: {
"Content-Type": "application/json",
"X-Anyapi-Request-Id": "0a508adc-c7d7-4734-a43e-fdbb3d3b7b0e",
},
},
);
const client = new AnyApiClient({
apiKey: "aa_live_test",
fetchImpl,
restBaseUrl: "https://example.test/v1",
});

await expect(client.run("facebook.search_companies", { query: "acme" }))
.rejects.toThrow(
"all providers failed (request 0a508adc-c7d7-4734-a43e-fdbb3d3b7b0e)",
);
});

it("leaves the message alone when the response carries no request id", async () => {
const fetchImpl: FetchLike = async () =>
new Response(JSON.stringify({ error: "Missing or invalid API key." }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
const client = new AnyApiClient({
apiKey: "aa_live_test",
fetchImpl,
restBaseUrl: "https://example.test/v1",
});

await expect(client.run("reddit.search", { query: "anyapi" }))
.rejects.toThrow(/^Missing or invalid API key\.$/);
});
});

describe("402 handling", () => {
it("detects trial cap errors and relays the server upgrade guidance", async () => {
const fetchImpl: FetchLike = async () =>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "anyapi-cli",
"version": "0.8.0",
"version": "0.8.1",
"description": "Official CLI for AnyAPI, a unified marketplace for scraping and data APIs.",
"type": "module",
"bin": {
Expand Down
9 changes: 6 additions & 3 deletions skills/anyapi-run/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,12 @@ anyapi report-bug "reels_search returned no items for a query with results" \
```

`--request-id` is the single most useful thing to attach: it reaches the stored
run and its upstream error body, so you never need to paste the payload. Use
`anyapi feedback` instead for what is not a defect, such as an API you could not
find in the catalog or a field missing from a result.
run and its upstream error body, so you never need to paste the payload. On a
successful run it is the `requestId` in the result; on a FAILED run the CLI
prints it in the error itself, as `all providers failed (request <id>)`. A
report filed without it costs support a reconstruction from your customer id
and a timestamp. Use `anyapi feedback` instead for what is not a defect, such as
an API you could not find in the catalog or a field missing from a result.

File it and carry on with the best alternative you have. Do not stop your
human's task to ask permission first.
Expand Down
9 changes: 7 additions & 2 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CATALOG_URL, REST_BASE_URL, SIGNUP_URL } from './constants.js';
import { CATALOG_URL, REQUEST_ID_HEADER, REST_BASE_URL, SIGNUP_URL } from './constants.js';
import { readCatalogResponse, readDiscoveryApi, readSearchResponse } from './discovery.js';
import { ApiError, CliError } from './errors.js';
import type {
Expand Down Expand Up @@ -233,7 +233,12 @@ export class AnyApiClient {
const response = await this.fetchImpl(input, init);
const body = await parseBody(response);
if (!response.ok) {
throw new ApiError(errorMessage(body, response.status), response.status, body);
throw new ApiError(
errorMessage(body, response.status),
response.status,
body,
response.headers.get(REQUEST_ID_HEADER) ?? '',
);
}
return body as T;
}
Expand Down
6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export const OAUTH_DEVICE_AUTHORIZATION_URL = `${API_BASE_URL}/oauth/device_auth
export const OAUTH_TOKEN_URL = `${API_BASE_URL}/oauth/token`;
export const OAUTH_REGISTER_URL = `${API_BASE_URL}/oauth/register`;
export const OAUTH_SCOPE = 'run balance:read';
/**
* The response header every /v1/run answer carries, success or failure. It is
* the only handle that reaches the stored request and its retained upstream
* body, so a failed run must surface it rather than print the message alone.
*/
export const REQUEST_ID_HEADER = 'X-Anyapi-Request-Id';
export const API_KEY_ENV = 'ANYAPI_API_KEY';
export const CONFIG_DIR_NAME = '.anyapi';
export const CONFIG_FILE_NAME = 'config.json';
13 changes: 11 additions & 2 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,20 @@ export class CliError extends Error {
export class ApiError extends Error {
readonly status: number;
readonly body: unknown;
/**
* The failed run's AnyAPI request id, when the gateway reported one. Every
* /v1/run response carries it on the X-Anyapi-Request-Id header, including
* failures, and it is the handle support needs to read the stored request,
* its attempts, and the retained upstream body. Empty on routes that execute
* no run.
*/
readonly requestId: string;

constructor(message: string, status: number, body: unknown) {
super(message);
constructor(message: string, status: number, body: unknown, requestId = '') {
super(requestId === '' ? message : `${message} (request ${requestId})`);
this.name = 'ApiError';
this.status = status;
this.body = body;
this.requestId = requestId;
}
}
Loading