Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- summary: |
Fix `PAYLOAD_TOO_LARGE` failures when publishing docs for large APIs with dynamic snippets. The
dynamic IRs were being sent inline in the `registerApiDefinition` request once per SDK language,
even though the registry only needs the language names to issue upload URLs (the IRs are uploaded
to S3 separately).
type: fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { toRegisterDynamicIRsInput } from "../toRegisterDynamicIRsInput.js";

describe("toRegisterDynamicIRsInput", () => {
it("returns undefined when there are no dynamic IRs", () => {
expect(toRegisterDynamicIRsInput(undefined)).toBeUndefined();
});

it("preserves the language keys", () => {
const result = toRegisterDynamicIRsInput({
python: { dynamicIR: { types: {} } },
typescript: { dynamicIR: { types: {} } }
});

expect(Object.keys(result ?? {}).sort()).toEqual(["python", "typescript"]);
});

it("strips the IR bodies so they are not sent in the registration request", () => {
const dynamicIR = { types: { User: { name: "User" } } };
const result = toRegisterDynamicIRsInput({ python: { dynamicIR }, go: { dynamicIR } });

expect(result).toEqual({ python: {}, go: {} });
expect(JSON.stringify(result)).not.toContain("User");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { normalizeRepoUrlToHttps } from "./normalizeRepoUrl.js";
import { publishDocsViaLedger } from "./publishDocsLedger.js";
import { publishDocsViaLedgerPreview } from "./publishDocsLedgerPreview.js";
import { retryWithBackoff } from "./retryWithBackoff.js";
import { toRegisterDynamicIRsInput } from "./toRegisterDynamicIRsInput.js";
import { asyncPool } from "./utils/asyncPool.js";

const MEASURE_IMAGE_BATCH_SIZE = 10;
Expand Down Expand Up @@ -487,7 +488,7 @@ export async function publishDocs({
orgId: CjsFdrSdk.OrgId(organization),
apiId: CjsFdrSdk.ApiId(effectiveApiName),
definition: apiDefinition,
dynamicIRs: dynamicIRsByLanguage
dynamicIRs: toRegisterDynamicIRsInput(dynamicIRsByLanguage)
}),
maxRetries: REGISTER_MAX_RETRIES,
baseDelayMs: REGISTER_BASE_DELAY_MS,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { APIV1Write } from "@fern-api/fdr-sdk";

type DynamicIr = APIV1Write.DynamicIr;

/**
* FDR only reads the language keys of `dynamicIRs` when registering an API definition — it mints one
* presigned upload URL per language, and the IRs themselves are uploaded directly to S3 afterwards.
* Sending the IR bodies inline duplicates the entire IR once per language in the registration request
* body, which can exceed the server's request size limit for large APIs.
*/
export function toRegisterDynamicIRsInput(
dynamicIRsByLanguage: Record<string, DynamicIr> | undefined
): Record<string, DynamicIr> | undefined {
if (dynamicIRsByLanguage == null) {
return undefined;
}
return Object.fromEntries(Object.keys(dynamicIRsByLanguage).map((language) => [language, {}]));
Comment on lines +11 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

Worth confirming that no deployed FDR version (including self-hosted registries pinned to older releases) consumes dynamicIRs[lang].dynamicIR from the registration body. If any do, this silently drops their dynamic snippets rather than failing loudly. If the S3 upload path is the only consumer everywhere, ignore.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked this in fern-platform: no FDR version has ever read the IR body from the register request.

  • dynamicIR first appears in FDR source in chore(fdr): separate dynamic ir uploads (31cf0be, fern-platform#3378) — before that the field didn't exist in the register contract at all, so older/self-hosted registries ignore it as an unknown field.
  • Since it was introduced, the only consumer is getDynamicIrsUploadsS3Service.getPresignedApiDefinitionDynamicIRsUploadUrls, which iterates Object.entries(dynamicIRs) and discards the value (for (const [language, _dynamicIr] of ...)) to mint one presigned URL per language. Reads go through loadDynamicIRFromS3, i.e. S3 is the only source of truth.
  • FDR's own registration tests already send dynamicIR: {} (servers/fdr/src/__test__/local/services/api.test.ts), and the schema is z.object({ dynamicIR: z.unknown() }), so an empty object is valid input rather than a validation failure.

So dropping the bodies can't silently lose snippets — a registry that failed to receive the IR would fail at the upload/read step, not silently.

}