From 208a8459f3439e59c972d8e924030f4035190a42 Mon Sep 17 00:00:00 2001 From: jiyuu-jin Date: Wed, 25 Jun 2025 10:07:20 -0400 Subject: [PATCH 1/2] Add better error handling on publish requests to API --- package.json | 2 +- src/api.ts | 137 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 130 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 3b4363d..b3428b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hyperplay/cli", - "version": "2.14.6", + "version": "2.14.7", "description": "Hyperplay CLI", "author": "HyperPlay Labs, Inc.", "bin": { diff --git a/src/api.ts b/src/api.ts index 0fd30ea..dec65ed 100644 --- a/src/api.ts +++ b/src/api.ts @@ -4,6 +4,7 @@ import { SiweMessage } from 'siwe'; import { Cookie, CookieJar } from 'tough-cookie'; import qs from "qs"; import { CliUx } from '@oclif/core'; +import { AxiosError } from 'axios'; export async function logCookiesAndCheckCsrf( cookieJar: CookieJar, @@ -61,15 +62,135 @@ export async function login(client: AxiosInstance, cookieJar: CookieJar, signer: CliUx.ux.action.stop(); } +interface ApiErrorResponse { + message?: string; +} + +function extractApiError(axiosError: AxiosError): { status: number; statusText: string; apiError: string } { + const status = axiosError.response?.status || 0; + const statusText = axiosError.response?.statusText || ''; + const responseData = axiosError.response?.data as unknown as ApiErrorResponse; + const apiError = responseData?.message || statusText || 'Unknown error'; + + return { status, statusText, apiError }; +} + +function createApiError(userMessage: string, apiError: string): Error { + return new Error(`${userMessage}\nAPI Error: ${apiError}`); +} + +function handleNetworkError(context: string): Error { + return new Error(`Network error ${context}.\nPlease check your internet connection and try again.`); +} + +function handleAxiosError(error: unknown, context: string, statusCodeHandlers: Record Error>): never { + const axiosError = error as AxiosError; + + if (axiosError.response) { + const { status, apiError } = extractApiError(axiosError); + const handler = statusCodeHandlers[status]; + + if (handler) { + throw handler(apiError); + } else { + const { statusText } = extractApiError(axiosError); + throw createApiError( + `${context} failed (HTTP ${status} ${statusText}).\nPlease contact support if this issue persists.`, + apiError + ); + } + } else if (axiosError.request) { + throw handleNetworkError(context); + } else { + throw new Error(`${context}: ${axiosError.message}`); + } +} + export async function publish(client: AxiosInstance, projectID: string, path: string, targetChannel: string) { - CliUx.ux.log('Fetching listing release branches'); - const channels = (await client.get<{ channel_id: number, channel_name: string }[]>(`/api/v1/channels?project_id=${projectID}`)).data; + CliUx.ux.log(`Publishing to project ID: ${projectID}`); + CliUx.ux.log(`Target channel: ${targetChannel}`); + CliUx.ux.log(`Release path: ${path}`); + + // Fetch available channels for the project + CliUx.ux.log('Fetching available release channels...'); + let channels: { channel_id: number, channel_name: string }[]; + + try { + const response = await client.get<{ channel_id: number, channel_name: string }[]>(`/api/v1/channels?project_id=${projectID}`); + channels = response.data; + + if (!Array.isArray(channels)) { + throw new Error('Invalid response format: expected array of channels'); + } + + CliUx.ux.log(`Found ${channels.length} available channels: ${channels.map(c => c.channel_name).join(', ')}`); + + } catch (error) { + handleAxiosError(error, 'Error fetching channels', { + 404: (apiError) => createApiError( + `Project not found. Please verify your project ID: ${projectID}\nMake sure the project exists and you have access to it.`, + apiError + ), + 403: (apiError) => createApiError( + `Access denied to project ${projectID}.\nPlease check that your account has permission to access this project.`, + apiError + ), + 401: (apiError) => createApiError( + 'Authentication failed. Please verify your private key and try again.', + apiError + ) + }); + } + + // Find the target channel const releaseChannel = channels.find((channel) => targetChannel === channel.channel_name); - if (!releaseChannel) CliUx.ux.warn(`Provided release channel_name ${targetChannel} not found on project!`); - CliUx.ux.log('Submitting release for review'); - await client.post("/api/v1/reviews/release", { - path, - channel_id: releaseChannel?.channel_id, - }); + if (!releaseChannel) { + const availableChannels = channels.map(c => c.channel_name).join(', '); + throw new Error(`Release channel "${targetChannel}" not found on this project.\n` + + `Available channels: ${availableChannels}\n` + + `Please use --channel flag with one of the available channel names.`); + } + + CliUx.ux.log(`Using channel: ${releaseChannel.channel_name} (ID: ${releaseChannel.channel_id})`); + + // Submit release for review + CliUx.ux.log('Submitting release for review...'); + + try { + await client.post("/api/v1/reviews/release", { + path, + channel_id: releaseChannel.channel_id, + }); + + CliUx.ux.log(`✅ Successfully submitted release for review on channel "${targetChannel}"`); + + } catch (error) { + handleAxiosError(error, 'Release submission', { + 400: (apiError) => createApiError( + 'Invalid request data.\nPlease check your release configuration.', + apiError + ), + 404: (apiError) => createApiError( + 'Release submission endpoint not found.\nThis may indicate an API version mismatch or the endpoint has changed.', + apiError + ), + 403: (apiError) => createApiError( + 'Access denied for release submission.\nPlease verify you have permission to publish releases on this project.', + apiError + ), + 401: (apiError) => createApiError( + 'Authentication expired. Please re-authenticate and try again.', + apiError + ), + 409: (apiError) => createApiError( + 'Release conflict: A release with this configuration may already exist.', + apiError + ), + 422: (apiError) => createApiError( + 'Release validation failed.\nPlease check your release files and metadata.', + apiError + ) + }); + } } From f9e0f9e574ef7059aa73395ea031d0ca4014112d Mon Sep 17 00:00:00 2001 From: jiyuu-jin Date: Wed, 25 Jun 2025 10:26:10 -0400 Subject: [PATCH 2/2] Cleanup imports --- src/api.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/api.ts b/src/api.ts index dec65ed..e6153ae 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,10 +1,9 @@ -import { AxiosInstance } from 'axios'; +import { AxiosInstance, AxiosError } from 'axios'; import { ethers } from 'ethers'; import { SiweMessage } from 'siwe'; import { Cookie, CookieJar } from 'tough-cookie'; import qs from "qs"; import { CliUx } from '@oclif/core'; -import { AxiosError } from 'axios'; export async function logCookiesAndCheckCsrf( cookieJar: CookieJar,