| @@ -12,6 +12,31 @@ export interface SpecFetchCredentials { · 11 unmodified lines |
| 12 | readonly queryParams?: Record<string, string>; | 12 | readonly queryParams?: Record<string, string>; |
| 13 | } | 13 | } |
| 14 | | 14 | |
| | 15 | export const MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH = 25 * 1024 * 1024; |
| | 16 | |
| | 17 | const formatMiB = (bytes: number): string => `${(bytes / 1024 / 1024).toFixed(1)} MiB`; |
| | 18 | |
| | 19 | const oversizedOpenApiSpecError = (size: number): OpenApiParseError => |
| | 20 | new OpenApiParseError({ |
| | 21 | message: `OpenAPI document is too large for the generic OpenAPI importer (${formatMiB(size)}; limit ${formatMiB(MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH)}). Use a provider-specific integration when available, or import a smaller filtered OpenAPI document.`, |
| | 22 | }); |
| | 23 | |
| | 24 | const parseContentLength = ( |
| | 25 | headers: Readonly<Record<string, string | undefined>>, |
| | 26 | ): number | undefined => { |
| | 27 | const raw = headers["content-length"] ?? headers["Content-Length"]; |
| | 28 | if (!raw) return undefined; |
| | 29 | const parsed = Number(raw); |
| | 30 | return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined; |
| | 31 | }; |
| | 32 | |
| | 33 | export const ensureGenericOpenApiSpecTextWithinLimit = ( |
| | 34 | specText: string, |
| | 35 | ): Effect.Effect<string, OpenApiParseError> => |
| | 36 | specText.length > MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH |
| | 37 | ? Effect.fail(oversizedOpenApiSpecError(specText.length)) |
| | 38 | : Effect.succeed(specText); |
| | 39 | |
| 15 | // ExtractionError subclass raised from parse() for non-3.x specs | 40 | // ExtractionError subclass raised from parse() for non-3.x specs |
| 16 | class OpenApiExtractionErrorFromParse extends OpenApiExtractionError {} | 41 | class OpenApiExtractionErrorFromParse extends OpenApiExtractionError {} |
| 17 | | 42 | |
| @@ -50,7 +75,11 @@ export const fetchSpecText = Effect.fn("OpenApi.fetchSpecText")(function* ( · 32 unmodified lines |
| 50 | message: `Failed to fetch OpenAPI document: HTTP ${response.status}`, | 75 | message: `Failed to fetch OpenAPI document: HTTP ${response.status}`, |
| 51 | }); | 76 | }); |
| 52 | } | 77 | } |
| 53 | return yield* response.text.pipe( | 78 | const contentLength = parseContentLength(response.headers); |
| | 79 | if (contentLength !== undefined && contentLength > MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH) { |
| | 80 | return yield* oversizedOpenApiSpecError(contentLength); |
| | 81 | } |
| | 82 | const specText = yield* response.text.pipe( |
| 54 | Effect.mapError( | 83 | Effect.mapError( |
| 55 | (_cause) => | 84 | (_cause) => |
| 56 | new OpenApiParseError({ | 85 | new OpenApiParseError({ |
| @@ -58,6 +87,7 @@ export const fetchSpecText = Effect.fn("OpenApi.fetchSpecText")(function* ( · 1 unmodified line |
| 58 | }), | 87 | }), |
| 59 | ), | 88 | ), |
| 60 | ); | 89 | ); |
| | 90 | return yield* ensureGenericOpenApiSpecTextWithinLimit(specText); |
| 61 | }); | 91 | }); |
| 62 | | 92 | |
| 63 | /** | 93 | /** |
| @@ -67,7 +97,7 @@ export const fetchSpecText = Effect.fn("OpenApi.fetchSpecText")(function* ( · 3 unmodified lines |
| 67 | export const resolveSpecText = (input: string, credentials?: SpecFetchCredentials) => | 97 | export const resolveSpecText = (input: string, credentials?: SpecFetchCredentials) => |
| 68 | input.startsWith("http://") || input.startsWith("https://") | 98 | input.startsWith("http://") || input.startsWith("https://") |
| 69 | ? fetchSpecText(input, credentials) | 99 | ? fetchSpecText(input, credentials) |
| 70 | : Effect.succeed(input); | 100 | : ensureGenericOpenApiSpecTextWithinLimit(input); |
| 71 | | 101 | |
| 72 | /** | 102 | /** |
| 73 | * Parse an OpenAPI document from spec text and validate it's OpenAPI 3.x. | 103 | * Parse an OpenAPI document from spec text and validate it's OpenAPI 3.x. |