fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: improve invalid input error

Lubos b643d7d4 5e1eaea7

+183 -45
+5
.changeset/mean-tips-care.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **cli**: improve error message on invalid input
+5
.changeset/rude-glasses-prove.md
··· 1 + --- 2 + "@hey-api/shared": patch 3 + --- 4 + 5 + **error**: handle InputError
+5
.changeset/seven-doors-learn.md
··· 1 + --- 2 + "@hey-api/json-schema-ref-parser": patch 3 + --- 4 + 5 + **internal**: export errors
+14
packages/json-schema-ref-parser/src/index.ts
··· 585 585 586 586 export { sendRequest } from './resolvers/url'; 587 587 export type { JSONSchema } from './types'; 588 + export type { JSONParserErrorType } from './util/errors'; 589 + export { 590 + InvalidPointerError, 591 + isHandledError, 592 + JSONParserError, 593 + JSONParserErrorGroup, 594 + MissingPointerError, 595 + normalizeError, 596 + ParserError, 597 + ResolverError, 598 + TimeoutError, 599 + UnmatchedParserError, 600 + UnmatchedResolverError, 601 + } from './util/errors';
+37 -20
packages/openapi-python/src/createClient.ts
··· 1 1 import path from 'node:path'; 2 2 3 3 import { type Logger, Project } from '@hey-api/codegen-core'; 4 - import { $RefParser } from '@hey-api/json-schema-ref-parser'; 4 + import { $RefParser, ResolverError } from '@hey-api/json-schema-ref-parser'; 5 + import type { Input, OpenApi, WatchValues } from '@hey-api/shared'; 5 6 import { 6 7 applyNaming, 7 8 buildGraph, 8 9 compileInputPath, 9 10 Context, 10 11 getSpec, 11 - type Input, 12 + InputError, 12 13 logInputPaths, 13 - type OpenApi, 14 14 parseOpenApiSpec, 15 15 patchOpenApiSpec, 16 16 postprocessOutput, 17 - type WatchValues, 18 17 } from '@hey-api/shared'; 19 18 import colors from 'ansi-colors'; 20 19 ··· 66 65 // if in watch mode, subsequent errors won't throw to gracefully handle 67 66 // cases where server might be reloading 68 67 if (error && !_watches) { 69 - const text = await response.text().catch(() => ''); 70 - throw new Error( 71 - `Request failed with status ${response.status}: ${text || response.statusText}`, 72 - ); 68 + const text = await response.text().catch((): string => ''); 69 + const message = `Request failed with status ${response.status}: ${text || response.statusText}`; 70 + // Handle 4xx client errors as input errors (bad URL, bad API key, etc.) 71 + if (response.status >= 400 && response.status < 500) { 72 + const statusText = response.statusText || 'Unknown'; 73 + const originalError = new Error(message) as Error & { source?: string }; 74 + originalError.source = String(inputPaths[index]!.path); 75 + throw new InputError( 76 + `Input request failed: ${response.status} ${statusText}`, 77 + originalError, 78 + ); 79 + } 80 + // For 5xx server errors, keep the generic error (could be a bug) 81 + throw new Error(message); 73 82 } 74 83 75 84 return { arrayBuffer, resolvedInput }; ··· 82 91 83 92 if (specData.length) { 84 93 const refParser = new $RefParser(); 85 - const data = 86 - specData.length > 1 87 - ? await refParser.bundleMany({ 88 - arrayBuffer: specData.map((data) => data.arrayBuffer!), 89 - pathOrUrlOrSchemas: [], 90 - resolvedInputs: specData.map((data) => data.resolvedInput!), 91 - }) 92 - : await refParser.bundle({ 93 - arrayBuffer: specData[0]!.arrayBuffer, 94 - pathOrUrlOrSchema: undefined, 95 - resolvedInput: specData[0]!.resolvedInput!, 96 - }); 94 + let data: unknown; 95 + try { 96 + data = 97 + specData.length > 1 98 + ? await refParser.bundleMany({ 99 + arrayBuffer: specData.map((data) => data.arrayBuffer!), 100 + pathOrUrlOrSchemas: [], 101 + resolvedInputs: specData.map((data) => data.resolvedInput!), 102 + }) 103 + : await refParser.bundle({ 104 + arrayBuffer: specData[0]!.arrayBuffer, 105 + pathOrUrlOrSchema: undefined, 106 + resolvedInput: specData[0]!.resolvedInput!, 107 + }); 108 + } catch (err) { 109 + if (err instanceof ResolverError && err.ioErrorCode === 'ENOENT') { 110 + throw new InputError('Input file not found', err); 111 + } 112 + throw err; 113 + } 97 114 98 115 // on subsequent runs in watch mode, print the message only if we know we're 99 116 // generating the output
+10 -2
packages/openapi-python/src/generate.ts
··· 6 6 import { 7 7 checkNodeVersion, 8 8 ConfigValidationError, 9 + getInputError, 9 10 getLogs, 10 11 JobError, 11 12 logCrashReport, ··· 100 101 rawLogs; 101 102 const dryRun = 102 103 jobs.some((job) => job.config.dryRun) ?? userConfigs.some((config) => config.dryRun) ?? false; 103 - const logPath = logs?.file && !dryRun ? logCrashReport(error, logs.path ?? '') : undefined; 104 + 105 + const inputError = getInputError(error); 106 + const normalizedError = inputError ?? error; 107 + 108 + const logPath = 109 + logs?.file && !dryRun ? logCrashReport(normalizedError, logs.path ?? '') : undefined; 104 110 if (!logs || logs.level !== 'silent') { 105 111 printCrashReport({ error, logPath }); 106 112 const isInteractive = 107 113 jobs.some((job) => job.config.interactive) ?? 108 114 userConfigs.some((config) => config.interactive) ?? 109 115 false; 110 - if (await shouldReportCrash({ error, isInteractive })) { 116 + if (await shouldReportCrash({ error: normalizedError, isInteractive })) { 111 117 await openGitHubIssueWithCrashReport(error, __dirname); 112 118 } 113 119 } 120 + 121 + if (inputError) return []; 114 122 115 123 throw error; 116 124 }
+36 -19
packages/openapi-ts/src/createClient.ts
··· 1 1 import path from 'node:path'; 2 2 3 3 import { type Logger, Project } from '@hey-api/codegen-core'; 4 - import { $RefParser } from '@hey-api/json-schema-ref-parser'; 4 + import { $RefParser, ResolverError } from '@hey-api/json-schema-ref-parser'; 5 + import type { Input, OpenApi, WatchValues } from '@hey-api/shared'; 5 6 import { 6 7 applyNaming, 7 8 buildGraph, 8 9 compileInputPath, 9 10 Context, 10 11 getSpec, 11 - type Input, 12 + InputError, 12 13 logInputPaths, 13 - type OpenApi, 14 14 parseOpenApiSpec, 15 15 patchOpenApiSpec, 16 16 postprocessOutput, 17 - type WatchValues, 18 17 } from '@hey-api/shared'; 19 18 import colors from 'ansi-colors'; 20 19 ··· 67 66 // cases where server might be reloading 68 67 if (error && !_watches) { 69 68 const text = await response.text().catch(() => ''); 70 - throw new Error( 71 - `Request failed with status ${response.status}: ${text || response.statusText}`, 72 - ); 69 + const message = `Request failed with status ${response.status}: ${text || response.statusText}`; 70 + // Handle 4xx client errors as input errors (bad URL, bad API key, etc.) 71 + if (response.status >= 400 && response.status < 500) { 72 + const statusText = response.statusText || 'Unknown'; 73 + const originalError = new Error(message) as Error & { source?: string }; 74 + originalError.source = String(inputPaths[index]!.path); 75 + throw new InputError( 76 + `Input request failed: ${response.status} ${statusText}`, 77 + originalError, 78 + ); 79 + } 80 + // For 5xx server errors, keep the generic error (could be a bug) 81 + throw new Error(message); 73 82 } 74 83 75 84 return { arrayBuffer, resolvedInput }; ··· 82 91 83 92 if (specData.length) { 84 93 const refParser = new $RefParser(); 85 - const data = 86 - specData.length > 1 87 - ? await refParser.bundleMany({ 88 - arrayBuffer: specData.map((data) => data.arrayBuffer!), 89 - pathOrUrlOrSchemas: [], 90 - resolvedInputs: specData.map((data) => data.resolvedInput!), 91 - }) 92 - : await refParser.bundle({ 93 - arrayBuffer: specData[0]!.arrayBuffer, 94 - pathOrUrlOrSchema: undefined, 95 - resolvedInput: specData[0]!.resolvedInput!, 96 - }); 94 + let data: unknown; 95 + try { 96 + data = 97 + specData.length > 1 98 + ? await refParser.bundleMany({ 99 + arrayBuffer: specData.map((data) => data.arrayBuffer!), 100 + pathOrUrlOrSchemas: [], 101 + resolvedInputs: specData.map((data) => data.resolvedInput!), 102 + }) 103 + : await refParser.bundle({ 104 + arrayBuffer: specData[0]!.arrayBuffer, 105 + pathOrUrlOrSchema: undefined, 106 + resolvedInput: specData[0]!.resolvedInput!, 107 + }); 108 + } catch (err) { 109 + if (err instanceof ResolverError && err.ioErrorCode === 'ENOENT') { 110 + throw new InputError('Input file not found', err); 111 + } 112 + throw err; 113 + } 97 114 98 115 // on subsequent runs in watch mode, print the message only if we know we're 99 116 // generating the output
+10 -2
packages/openapi-ts/src/generate.ts
··· 6 6 import { 7 7 checkNodeVersion, 8 8 ConfigValidationError, 9 + getInputError, 9 10 getLogs, 10 11 JobError, 11 12 logCrashReport, ··· 100 101 rawLogs; 101 102 const dryRun = 102 103 jobs.some((job) => job.config.dryRun) ?? userConfigs.some((config) => config.dryRun) ?? false; 103 - const logPath = logs?.file && !dryRun ? logCrashReport(error, logs.path ?? '') : undefined; 104 + 105 + const inputError = getInputError(error); 106 + const normalizedError = inputError ?? error; 107 + 108 + const logPath = 109 + logs?.file && !dryRun ? logCrashReport(normalizedError, logs.path ?? '') : undefined; 104 110 if (!logs || logs.level !== 'silent') { 105 111 printCrashReport({ error, logPath }); 106 112 const isInteractive = 107 113 jobs.some((job) => job.config.interactive) ?? 108 114 userConfigs.some((config) => config.interactive) ?? 109 115 false; 110 - if (await shouldReportCrash({ error, isInteractive })) { 116 + if (await shouldReportCrash({ error: normalizedError, isInteractive })) { 111 117 await openGitHubIssueWithCrashReport(error, __dirname); 112 118 } 113 119 } 120 + 121 + if (inputError) return []; 114 122 115 123 throw error; 116 124 }
+59 -2
packages/shared/src/error.ts
··· 38 38 } 39 39 40 40 /** 41 + * Represents an error caused by invalid or inaccessible input. 42 + * 43 + * Used for errors like file not found, URL not reachable, etc. 44 + */ 45 + export class InputError extends Error { 46 + readonly originalError: Error & { source?: string }; 47 + 48 + constructor(message: string, originalError: Error & { source?: string }) { 49 + super(message); 50 + this.name = 'InputError'; 51 + this.originalError = originalError; 52 + } 53 + } 54 + 55 + /** 41 56 * Represents a runtime error originating from a specific job. 42 57 * 43 58 * Used for reporting job-level failures that are not config validation errors. ··· 83 98 } 84 99 85 100 export function logCrashReport(error: unknown, logsDir: string): string | undefined { 86 - if (error instanceof ConfigError || error instanceof ConfigValidationError) { 101 + if ( 102 + error instanceof ConfigError || 103 + error instanceof ConfigValidationError || 104 + error instanceof InputError 105 + ) { 87 106 return; 88 107 } 89 108 ··· 157 176 await open(url); 158 177 } 159 178 179 + export function getInputError(error: unknown): InputError | undefined { 180 + if (error instanceof InputError) { 181 + return error; 182 + } 183 + if (error instanceof JobError && error.originalError.error instanceof InputError) { 184 + return error.originalError.error; 185 + } 186 + } 187 + 160 188 export function printCrashReport({ 161 189 error, 162 190 logPath, ··· 193 221 error = error.originalError.error; 194 222 } 195 223 224 + if (error instanceof InputError) { 225 + const source = (error.originalError as { source?: string }).source; 226 + const itemPrefixStr = ` `; 227 + 228 + const isNetworkError = error.message.startsWith('Input request failed'); 229 + if (isNetworkError) { 230 + console.error(`${jobPrefix}${colors.red(`❌ ${error.message}`)}`); 231 + if (source) console.error(colors.gray(source)); 232 + console.error(colors.gray('\nPlease verify that:')); 233 + console.error(colors.gray(`${itemPrefixStr}• The URL is correct`)); 234 + console.error(colors.gray(`${itemPrefixStr}• Your API key is valid`)); 235 + console.error(colors.gray(`${itemPrefixStr}• You have network access`)); 236 + return; 237 + } 238 + 239 + console.error(`${jobPrefix}${colors.red('❌ Input file not found:')}`); 240 + if (source) console.error(colors.gray(source)); 241 + console.error(colors.gray('\nPlease verify that:')); 242 + console.error(colors.gray(`${itemPrefixStr}• The file exists`)); 243 + console.error(colors.gray(`${itemPrefixStr}• The path is correct`)); 244 + console.error(colors.gray(`${itemPrefixStr}• You have read permissions`)); 245 + return; 246 + } 247 + 196 248 const baseString = colors.red('Failed with the message:'); 197 249 console.error(`${jobPrefix}❌ ${baseString}`); 198 250 const itemPrefixStr = ` `; ··· 215 267 error: unknown; 216 268 isInteractive: boolean | undefined; 217 269 }): Promise<boolean> { 218 - if (!isInteractive || error instanceof ConfigError || error instanceof ConfigValidationError) { 270 + if ( 271 + !isInteractive || 272 + error instanceof ConfigError || 273 + error instanceof ConfigValidationError || 274 + error instanceof InputError 275 + ) { 219 276 return false; 220 277 } 221 278
+2
packages/shared/src/index.ts
··· 35 35 export { 36 36 ConfigError, 37 37 ConfigValidationError, 38 + getInputError, 38 39 HeyApiError, 40 + InputError, 39 41 JobError, 40 42 logCrashReport, 41 43 openGitHubIssueWithCrashReport,