mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-19 16:39:50 +00:00
fix(digitalocean): throw on non-2xx in doApi() wrapper (#2112)
* fix(digitalocean): throw on non-2xx in doApi() wrapper Make doApi() throw on non-2xx responses, matching hetznerApi and daytonaApi. 5/7 call sites were silently swallowing 401/403/404/422 errors by only destructuring text and ignoring status. Agent: code-health Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style: fix biome formatting in doApi() signature Function signature needed multi-line format to match biome expectations. Agent: code-health Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0e145c2e8a
commit
97a92f3d4f
1 changed files with 27 additions and 41 deletions
|
|
@ -92,15 +92,7 @@ let doServerIp = "";
|
|||
|
||||
// ─── API Client ──────────────────────────────────────────────────────────────
|
||||
|
||||
async function doApi(
|
||||
method: string,
|
||||
endpoint: string,
|
||||
body?: string,
|
||||
maxRetries = 3,
|
||||
): Promise<{
|
||||
status: number;
|
||||
text: string;
|
||||
}> {
|
||||
async function doApi(method: string, endpoint: string, body?: string, maxRetries = 3): Promise<string> {
|
||||
const url = `${DO_API_BASE}${endpoint}`;
|
||||
|
||||
let interval = 2;
|
||||
|
|
@ -129,10 +121,10 @@ async function doApi(
|
|||
interval = Math.min(interval * 2, 30);
|
||||
continue;
|
||||
}
|
||||
return {
|
||||
status: resp.status,
|
||||
text,
|
||||
};
|
||||
if (!resp.ok) {
|
||||
throw new Error(`DigitalOcean API error ${resp.status} for ${method} ${endpoint}: ${text.slice(0, 200)}`);
|
||||
}
|
||||
return text;
|
||||
} catch (err) {
|
||||
if (attempt >= maxRetries) {
|
||||
throw err;
|
||||
|
|
@ -234,7 +226,7 @@ async function testDoToken(): Promise<boolean> {
|
|||
return false;
|
||||
}
|
||||
try {
|
||||
const { text } = await doApi("GET", "/account", undefined, 1);
|
||||
const text = await doApi("GET", "/account", undefined, 1);
|
||||
return text.includes('"uuid"');
|
||||
} catch {
|
||||
return false;
|
||||
|
|
@ -601,8 +593,8 @@ export async function ensureSshKey(): Promise<void> {
|
|||
}
|
||||
|
||||
// Check if key is registered with DigitalOcean
|
||||
const { text } = await doApi("GET", "/account/keys");
|
||||
const data = parseJsonObj(text);
|
||||
const keysText = await doApi("GET", "/account/keys");
|
||||
const data = parseJsonObj(keysText);
|
||||
const keys = toObjectArray(data?.ssh_keys);
|
||||
|
||||
const found = keys.some((k: Record<string, unknown>) => {
|
||||
|
|
@ -622,16 +614,22 @@ export async function ensureSshKey(): Promise<void> {
|
|||
name: `spawn-${key.name}`,
|
||||
public_key: pubKey,
|
||||
});
|
||||
const { text: regText } = await doApi("POST", "/account/keys", body);
|
||||
|
||||
if (regText.includes('"id"')) {
|
||||
logInfo(`SSH key '${key.name}' registered with DigitalOcean`);
|
||||
let regText: string;
|
||||
try {
|
||||
regText = await doApi("POST", "/account/keys", body);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
// Key may already exist under a different name — non-fatal
|
||||
if (msg.includes("already been taken") || msg.includes("already in use")) {
|
||||
logInfo(`SSH key '${key.name}' already registered (under a different name)`);
|
||||
continue;
|
||||
}
|
||||
logWarn(`SSH key '${key.name}' registration may have failed, continuing...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key may already exist under a different name — non-fatal
|
||||
if (regText.includes("already been taken") || regText.includes("already in use")) {
|
||||
logInfo(`SSH key '${key.name}' already registered (under a different name)`);
|
||||
if (regText.includes('"id"')) {
|
||||
logInfo(`SSH key '${key.name}' registered with DigitalOcean`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -813,7 +811,7 @@ export async function createServer(
|
|||
logStep(`Creating DigitalOcean droplet '${name}' (size: ${size}, region: ${effectiveRegion})...`);
|
||||
|
||||
// Get all SSH key IDs
|
||||
const { text: keysText } = await doApi("GET", "/account/keys");
|
||||
const keysText = await doApi("GET", "/account/keys");
|
||||
const keysData = parseJsonObj(keysText);
|
||||
const sshKeyIds: number[] = toObjectArray(keysData?.ssh_keys)
|
||||
.map((k) => (isNumber(k.id) ? k.id : 0))
|
||||
|
|
@ -831,7 +829,7 @@ export async function createServer(
|
|||
monitoring: false,
|
||||
});
|
||||
|
||||
const { text: createText } = await doApi("POST", "/droplets", body);
|
||||
const createText = await doApi("POST", "/droplets", body);
|
||||
const createData = parseJsonObj(createText);
|
||||
|
||||
if (!createData?.droplet?.id) {
|
||||
|
|
@ -858,7 +856,7 @@ async function waitForDropletActive(dropletId: string, maxAttempts = 60): Promis
|
|||
logStep("Waiting for droplet to become active...");
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
const { text } = await doApi("GET", `/droplets/${dropletId}`);
|
||||
const text = await doApi("GET", `/droplets/${dropletId}`);
|
||||
const data = parseJsonObj(text);
|
||||
const status = data?.droplet?.status;
|
||||
|
||||
|
|
@ -1157,19 +1155,7 @@ export async function destroyServer(dropletId?: string): Promise<void> {
|
|||
}
|
||||
|
||||
logStep(`Destroying DigitalOcean droplet ${id}...`);
|
||||
const { status, text } = await doApi("DELETE", `/droplets/${id}`);
|
||||
|
||||
// DELETE returns 204 No Content on success (empty body)
|
||||
if (status === 204) {
|
||||
logInfo(`Droplet ${id} destroyed`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Any non-204 status is a failure — extract the best error message available
|
||||
const data = parseJsonObj(text);
|
||||
const errMsg = isString(data?.message) ? data.message : text.slice(0, 200) || `HTTP ${status}`;
|
||||
logError(`Failed to destroy droplet ${id}: ${errMsg}`);
|
||||
logWarn("The droplet may still be running and incurring charges.");
|
||||
logWarn(`Delete it manually at: ${DO_DASHBOARD_URL}`);
|
||||
throw new Error("Droplet deletion failed");
|
||||
// doApi throws on non-2xx; DELETE returns 204 No Content on success
|
||||
await doApi("DELETE", `/droplets/${id}`);
|
||||
logInfo(`Droplet ${id} destroyed`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue