History

Guard generic OpenAPI imports by size

Rhys Sullivan committed 4 months ago

Commit 82561081 parent f80115d3 files · +54 −5Browse files

Unified · Split

3 files
  1. packages/plugins/openapi/src/sdk/parse.test.ts +19 −1
  2. packages/plugins/openapi/src/sdk/parse.ts +32 −2
  3. packages/plugins/openapi/src/sdk/plugin.ts +3 −2
packages/plugins/openapi/src/sdk/parse.test.ts+19−1
@@ -2,7 +2,11 @@ import { describe, expect, it } from "@effect/vitest"; · 1 unmodified line
2import { Effect } from "effect";2import { Effect } from "effect";
3 3
4import { OpenApiParseError } from "./errors";4import { OpenApiParseError } from "./errors";
5import { parse } from "./parse";5import {
6 ensureGenericOpenApiSpecTextWithinLimit,
7 MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH,
8 parse,
9} from "./parse";
6 10
7describe("OpenAPI parse", () => {11describe("OpenAPI parse", () => {
8 it.effect("parses JSON OpenAPI documents", () =>12 it.effect("parses JSON OpenAPI documents", () =>
@@ -64,4 +68,18 @@ paths: {} · 55 unmodified lines
64 expect(error).toHaveProperty("message", "OpenAPI document must parse to an object");68 expect(error).toHaveProperty("message", "OpenAPI document must parse to an object");
65 }),69 }),
66 );70 );
71
72 it.effect("rejects oversized generic OpenAPI documents before parsing", () =>
73 Effect.gen(function* () {
74 const error = yield* ensureGenericOpenApiSpecTextWithinLimit(
75 "x".repeat(MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH + 1),
76 ).pipe(Effect.flip);
77
78 expect(error).toBeInstanceOf(OpenApiParseError);
79 expect(error).toHaveProperty(
80 "message",
81 expect.stringContaining("too large for the generic OpenAPI importer"),
82 );
83 }),
84 );
67});85});
packages/plugins/openapi/src/sdk/parse.ts+32−2
@@ -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
15export const MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH = 25 * 1024 * 1024;
16
17const formatMiB = (bytes: number): string => `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
18
19const 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
24const 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
33export 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 specs40// ExtractionError subclass raised from parse() for non-3.x specs
16class OpenApiExtractionErrorFromParse extends OpenApiExtractionError {}41class 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
67export const resolveSpecText = (input: string, credentials?: SpecFetchCredentials) =>97export 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.
packages/plugins/openapi/src/sdk/plugin.ts+3−2
@@ -22,7 +22,7 @@ import { · 21 unmodified lines
22 22
23import { decodeOpenApiIntegrationConfig, type OpenApiIntegrationConfig } from "./config";23import { decodeOpenApiIntegrationConfig, type OpenApiIntegrationConfig } from "./config";
24import { OpenApiExtractionError, OpenApiOAuthError, OpenApiParseError } from "./errors";24import { OpenApiExtractionError, OpenApiOAuthError, OpenApiParseError } from "./errors";
25import { parse, resolveSpecText } from "./parse";25import { ensureGenericOpenApiSpecTextWithinLimit, parse, resolveSpecText } from "./parse";
26import {26import {
27 convertGoogleDiscoveryBundleToOpenApi,27 convertGoogleDiscoveryBundleToOpenApi,
28 convertGoogleDiscoveryToOpenApi,28 convertGoogleDiscoveryToOpenApi,
@@ -473,7 +473,8 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 444 unmodified lines
473 const specText = yield* resolveSpecText(spec.url).pipe(Effect.provide(httpClientLayer));473 const specText = yield* resolveSpecText(spec.url).pipe(Effect.provide(httpClientLayer));
474 return { specText };474 return { specText };
475 }475 }
476 return { specText: spec.value };476 const specText = yield* ensureGenericOpenApiSpecTextWithinLimit(spec.value);
477 return { specText };
477 });478 });
478 479
479 return {480 return {