Skip to content

Commit bd1f7fd

Browse files
committed
Retries on transient auth errors
1 parent 2208632 commit bd1f7fd

1 file changed

Lines changed: 85 additions & 23 deletions

File tree

twister/cli/commands/deploy.ts

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,29 @@ import * as out from "../utils/output";
1010
import { handleSSEStream } from "../utils/sse";
1111
import { resolveToken } from "../utils/token.js";
1212

13+
async function fetchWithRetry(
14+
url: string,
15+
options: RequestInit,
16+
maxRetries = 3
17+
): Promise<Response> {
18+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
19+
try {
20+
const response = await fetch(url, options);
21+
if (response.status < 500 || attempt === maxRetries) {
22+
return response;
23+
}
24+
// 5xx — retry with backoff
25+
const delay = 1000 * Math.pow(2, attempt);
26+
await new Promise((r) => setTimeout(r, delay));
27+
} catch (error) {
28+
if (attempt === maxRetries) throw error;
29+
const delay = 1000 * Math.pow(2, attempt);
30+
await new Promise((r) => setTimeout(r, delay));
31+
}
32+
}
33+
throw new Error("Retry logic error");
34+
}
35+
1336
// Publisher types for API interaction
1437
interface Publisher {
1538
id: number;
@@ -65,7 +88,7 @@ async function createNewPublisher(
6588
// Try to get user's name for default
6689
let defaultName = "";
6790
try {
68-
const userResponse = await fetch(`${apiUrl}/v1/twist/user`, {
91+
const userResponse = await fetchWithRetry(`${apiUrl}/v1/twist/user`, {
6992
method: "GET",
7093
headers: {
7194
Authorization: `Bearer ${deployToken}`,
@@ -83,6 +106,12 @@ async function createNewPublisher(
83106
"Your login token is invalid or has expired. Please run 'plot login' to authenticate."
84107
);
85108
process.exit(1);
109+
} else if (userResponse.status >= 500) {
110+
out.error(
111+
"Server error",
112+
"The Plot API is temporarily unavailable. Please try again."
113+
);
114+
process.exit(1);
86115
}
87116
} catch (error) {
88117
// Ignore error, just won't have default
@@ -130,17 +159,20 @@ async function createNewPublisher(
130159
try {
131160
out.progress(`Creating publisher "${publisherName}"...`);
132161

133-
const createResponse = await fetch(`${apiUrl}/v1/twist/publishers`, {
134-
method: "POST",
135-
headers: {
136-
"Content-Type": "application/json",
137-
Authorization: `Bearer ${deployToken}`,
138-
},
139-
body: JSON.stringify({
140-
name: publisherName,
141-
url: response.url || null,
142-
} as NewPublisher),
143-
});
162+
const createResponse = await fetchWithRetry(
163+
`${apiUrl}/v1/twist/publishers`,
164+
{
165+
method: "POST",
166+
headers: {
167+
"Content-Type": "application/json",
168+
Authorization: `Bearer ${deployToken}`,
169+
},
170+
body: JSON.stringify({
171+
name: publisherName,
172+
url: response.url || null,
173+
} as NewPublisher),
174+
}
175+
);
144176

145177
if (!createResponse.ok) {
146178
if (createResponse.status === 401) {
@@ -150,6 +182,13 @@ async function createNewPublisher(
150182
);
151183
process.exit(1);
152184
}
185+
if (createResponse.status >= 500) {
186+
out.error(
187+
"Server error",
188+
"The Plot API is temporarily unavailable. Please try again."
189+
);
190+
process.exit(1);
191+
}
153192
const errorText = await createResponse.text();
154193
out.error("Failed to create publisher", errorText);
155194
process.exit(1);
@@ -312,7 +351,7 @@ export async function deployCommand(options: DeployOptions) {
312351
const urlPath = deploymentId || "personal";
313352

314353
try {
315-
const twistInfoResponse = await fetch(
354+
const twistInfoResponse = await fetchWithRetry(
316355
`${options.apiUrl}/v1/twist/${urlPath}`,
317356
{
318357
method: "GET",
@@ -336,6 +375,12 @@ export async function deployCommand(options: DeployOptions) {
336375
"Your login token is invalid or has expired. Please run 'plot login' to authenticate."
337376
);
338377
process.exit(1);
378+
} else if (twistInfoResponse.status >= 500) {
379+
out.error(
380+
"Server error",
381+
"The Plot API is temporarily unavailable. Please try again."
382+
);
383+
process.exit(1);
339384
} else if (twistInfoResponse.status !== 404) {
340385
// Log non-404 errors, but continue with publisher setup
341386
const errorText = await twistInfoResponse.text();
@@ -359,7 +404,7 @@ export async function deployCommand(options: DeployOptions) {
359404

360405
try {
361406
// Fetch accessible publishers
362-
const publishersResponse = await fetch(
407+
const publishersResponse = await fetchWithRetry(
363408
`${options.apiUrl}/v1/twist/publishers`,
364409
{
365410
method: "GET",
@@ -378,6 +423,13 @@ export async function deployCommand(options: DeployOptions) {
378423
);
379424
process.exit(1);
380425
}
426+
if (publishersResponse.status >= 500) {
427+
out.error(
428+
"Server error",
429+
"The Plot API is temporarily unavailable. Please try again."
430+
);
431+
process.exit(1);
432+
}
381433
const errorText = await publishersResponse.text();
382434
out.warning("Failed to fetch publishers", [errorText]);
383435
} else {
@@ -520,15 +572,18 @@ export async function deployCommand(options: DeployOptions) {
520572
const urlPath = deploymentId || "personal";
521573

522574
try {
523-
const response = await fetch(`${options.apiUrl}/v1/twist/${urlPath}`, {
524-
method: "POST",
525-
headers: {
526-
"Content-Type": "application/json",
527-
Accept: "text/event-stream",
528-
Authorization: `Bearer ${deployToken}`,
529-
},
530-
body: JSON.stringify(requestBody),
531-
});
575+
const response = await fetchWithRetry(
576+
`${options.apiUrl}/v1/twist/${urlPath}`,
577+
{
578+
method: "POST",
579+
headers: {
580+
"Content-Type": "application/json",
581+
Accept: "text/event-stream",
582+
Authorization: `Bearer ${deployToken}`,
583+
},
584+
body: JSON.stringify(requestBody),
585+
}
586+
);
532587

533588
if (!response.ok) {
534589
if (response.status === 401) {
@@ -538,6 +593,13 @@ export async function deployCommand(options: DeployOptions) {
538593
);
539594
process.exit(1);
540595
}
596+
if (response.status >= 500) {
597+
out.error(
598+
"Server error",
599+
"The Plot API is temporarily unavailable. Please try again."
600+
);
601+
process.exit(1);
602+
}
541603
const errorText = await response.text();
542604
out.error("Upload failed", errorText);
543605
process.exit(1);

0 commit comments

Comments
 (0)