fix: harden Linear integration helper against transient non-JSON errors

This commit is contained in:
Harsh 2026-03-06 15:08:54 +05:30
parent a0f16b161e
commit aa2bda2e32
1 changed files with 70 additions and 46 deletions

View File

@ -48,50 +48,72 @@ function linearGraphQL<T>(query: string, variables: Record<string, unknown>): Pr
}
const body = JSON.stringify({ query, variables });
return new Promise<T>((resolve, reject) => {
const url = new URL("https://api.linear.app/graphql");
const req = request(
{
hostname: url.hostname,
path: url.pathname,
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: LINEAR_API_KEY,
"Content-Length": Buffer.byteLength(body),
},
},
(res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer) => chunks.push(chunk));
res.on("end", () => {
try {
const text = Buffer.concat(chunks).toString("utf-8");
const json = JSON.parse(text) as {
data?: T;
errors?: Array<{ message: string }>;
};
if (json.errors?.length) {
reject(new Error(`Linear API error: ${json.errors[0].message}`));
return;
}
resolve(json.data as T);
} catch (err) {
reject(err);
}
async function executeWithRetry(attempt = 1): Promise<T> {
try {
return await new Promise<T>((resolve, reject) => {
const url = new URL("https://api.linear.app/graphql");
const req = request(
{
hostname: url.hostname,
path: url.pathname,
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: LINEAR_API_KEY,
"Content-Length": Buffer.byteLength(body),
},
},
(res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer) => chunks.push(chunk));
res.on("end", () => {
const text = Buffer.concat(chunks).toString("utf-8");
const statusCode = res.statusCode ?? 0;
if (statusCode >= 500) {
reject(new Error(`Linear API ${statusCode}: ${text.slice(0, 200)}`));
return;
}
let json: { data?: T; errors?: Array<{ message: string }> };
try {
json = JSON.parse(text) as { data?: T; errors?: Array<{ message: string }> };
} catch {
reject(
new Error(`Linear API returned non-JSON (${statusCode}): ${text.slice(0, 200)}`),
);
return;
}
if (json.errors?.length) {
reject(new Error(`Linear API error: ${json.errors[0].message}`));
return;
}
resolve(json.data as T);
});
},
);
req.setTimeout(30_000, () => {
req.destroy();
reject(new Error("Linear API request timed out"));
});
},
);
req.setTimeout(30_000, () => {
req.destroy();
reject(new Error("Linear API request timed out"));
});
req.on("error", (err) => reject(err));
req.write(body);
req.end();
});
} catch (err) {
if (attempt < 3) {
await new Promise((resolve) => setTimeout(resolve, 1_000));
return executeWithRetry(attempt + 1);
}
throw err;
}
}
req.on("error", (err) => reject(err));
req.write(body);
req.end();
});
return executeWithRetry();
}
// ---------------------------------------------------------------------------
@ -135,11 +157,13 @@ describe.skipIf(!canRun)("tracker-linear (integration)", () => {
// Resolve the UUID for cleanup — only possible with direct API key
if (LINEAR_API_KEY) {
const data = await linearGraphQL<{ issue: { id: string } }>(
`query($id: String!) { issue(id: $id) { id } }`,
{ id: issueIdentifier },
);
issueUuid = data.issue.id;
try {
const data = await linearGraphQL<{ issue: { id: string } }>(
`query($id: String!) { issue(id: $id) { id } }`,
{ id: issueIdentifier },
);
issueUuid = data.issue.id;
} catch {}
}
}, 30_000);