Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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,4 @@
- summary: |
Gzip-compress docs-ledger register and finish request bodies to reduce
transfer time for large multi-locale deployments.
type: feat
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- summary: |
Gzip-compress v2 docs publish request bodies (register, finish, API
registration, translation registration) to reduce transfer time for
large deployments.
type: feat
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Returns a `fetch` function that gzip-compresses request bodies using the
* Web Streams `CompressionStream` API.
*
* Designed to be captured by oRPC's `LinkFetchClient` at construction time
* so all subsequent requests through the client are transparently compressed.
*
* The FDR server registers `@fastify/compress` which transparently
* decompresses incoming `Content-Encoding: gzip` request bodies.
*/
export function createGzipFetch(): typeof globalThis.fetch {
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
if (input instanceof Request && input.body != null) {
const headers = new Headers(input.headers);
headers.set("Content-Encoding", "gzip");
headers.delete("Content-Length");

const compressedBody = input.body.pipeThrough(new CompressionStream("gzip"));

const compressedRequest = new Request(input.url, {
method: input.method,
headers,
body: compressedBody,
// @ts-expect-error duplex required for streaming request bodies in Node.js
duplex: "half"
});
return globalThis.fetch(compressedRequest, init);
}

return globalThis.fetch(input, init);
};
}

/**
* Gzip-compresses a JSON body and returns a `RequestInit` suitable for
* `fetch()`, with the correct `Content-Encoding` and `Content-Type` headers.
*/
export async function gzipJsonBody(body: unknown): Promise<{ body: ReadableStream; headers: Record<string, string> }> {
const json = JSON.stringify(body);
const stream = new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"));
return {
body: stream,
headers: {
"Content-Encoding": "gzip",
"Content-Type": "application/json"
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import * as mime from "mime-types";
import { basename } from "path";
import terminalLink from "terminal-link";
import { buildTranslatedDocsDefinition } from "./buildTranslatedDocsDefinition.js";
import { createGzipFetch, gzipJsonBody } from "./compressedFetch.js";
import { getDocsDeployMode } from "./docsDeployMode.js";
import { getDynamicGeneratorConfig } from "./getDynamicGeneratorConfig.js";
import { measureImageSizes } from "./measureImageSizes.js";
Expand Down Expand Up @@ -209,10 +210,17 @@ export async function publishDocs({
context.logger.info(`Docs deploy mode: ${deployMode}`);
}

// Capture a gzip-compressing fetch into the oRPC client so all FDR
// requests with a body are transparently compressed. LinkFetchClient
// snapshots globalThis.fetch at construction time, so we swap it in
// before building the client and restore immediately after.
const savedFetch = globalThis.fetch;
globalThis.fetch = createGzipFetch();
const fdr = createFdrService({
token: token.value,
...(Object.keys(headers).length > 0 && { headers })
});
globalThis.fetch = savedFetch;
Comment on lines +217 to +223
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Critical: Global state corruption on error

If createFdrService() throws an exception, globalThis.fetch will remain permanently modified to the gzip-compressing version, affecting all subsequent fetch calls in the process. This will cause subsequent non-compressed endpoints to receive gzipped data they don't expect.

Fix: Wrap in try/finally:

const savedFetch = globalThis.fetch;
try {
    globalThis.fetch = createGzipFetch();
    const fdr = createFdrService({
        token: token.value,
        ...(Object.keys(headers).length > 0 && { headers })
    });
} finally {
    globalThis.fetch = savedFetch;
}
Suggested change
const savedFetch = globalThis.fetch;
globalThis.fetch = createGzipFetch();
const fdr = createFdrService({
token: token.value,
...(Object.keys(headers).length > 0 && { headers })
});
globalThis.fetch = savedFetch;
const savedFetch = globalThis.fetch;
let fdr;
try {
globalThis.fetch = createGzipFetch();
fdr = createFdrService({
token: token.value,
...(Object.keys(headers).length > 0 && { headers })
});
} finally {
globalThis.fetch = savedFetch;
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

const authConfig = { type: "public" as const };

if (excludeApis) {
Expand Down Expand Up @@ -883,24 +891,28 @@ export async function publishDocs({
);
// Use a raw fetch instead of the oRPC client to send `docsDefinition`
// (the live server expects that field; the published fdr-sdk still uses `content`).
const translationPayload = {
domain: translationDomain,
// Send customDomains in production so FDR fans the translation
// S3 write out across every URL the docs are published to.
// Skipped in preview because preview deploys to a single
// ephemeral URL with no custom-domain mirrors.
customDomains: preview ? [] : customDomains,
orgId: organization,
locale,
docsDefinition: translatedDefinition
};
const compressed = await gzipJsonBody(translationPayload);
const translationResponse = await fetch(`${fdrOrigin}/v2/registry/docs/translations/register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...compressed.headers,
Authorization: `Bearer ${token.value}`,
...headers // Include telemetry headers (X-CLI-Version, X-CI-Source, etc.)
},
body: JSON.stringify({
domain: translationDomain,
// Send customDomains in production so FDR fans the translation
// S3 write out across every URL the docs are published to.
// Skipped in preview because preview deploys to a single
// ephemeral URL with no custom-domain mirrors.
customDomains: preview ? [] : customDomains,
orgId: organization,
locale,
docsDefinition: translatedDefinition
})
body: compressed.body,
// @ts-expect-error duplex required for streaming request bodies in Node.js
duplex: "half"
});
if (!translationResponse.ok) {
const body = await translationResponse.text();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { createHash } from "crypto";
import { readFile } from "fs/promises";

import { buildTranslatedDocsDefinition } from "./buildTranslatedDocsDefinition.js";
import { createGzipFetch } from "./compressedFetch.js";
import { mapDocsConfigToLedgerConfig } from "./mapDocsConfigToLedgerConfig.js";
import { asyncPool } from "./utils/asyncPool.js";

Expand Down Expand Up @@ -260,7 +261,7 @@ export async function publishDocsViaLedger({
// entry and translations follow. The server processes all locales
// through the same pipeline.

const client = createDocsLedgerClient({ baseUrl: fdrOrigin, token, headers });
const client = createDocsLedgerClient({ baseUrl: fdrOrigin, token, headers, fetch: createGzipFetch() });

const locales: LocaleEntry[] = [baseLocale, ...builtTranslations.map((t) => t.localeEntry)];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { AbsoluteFilePath } from "@fern-api/fs-utils";
import type { TaskContext } from "@fern-api/task-context";

import { buildTranslatedDocsDefinition } from "./buildTranslatedDocsDefinition.js";
import { createGzipFetch } from "./compressedFetch.js";
import { buildLedgerInput, uploadMissingBlobs } from "./publishDocsLedger.js";

type DocsDefinition = DocsV1Write.DocsDefinition;
Expand Down Expand Up @@ -87,7 +88,7 @@ export async function publishDocsViaLedgerPreview({

// ── Phase 2: Single register → upload → finish ─────────────────────

const client = createDocsLedgerClient({ baseUrl: fdrOrigin, token, headers });
const client = createDocsLedgerClient({ baseUrl: fdrOrigin, token, headers, fetch: createGzipFetch() });

context.logger.debug("[ledger-preview] Registering preview deployment...");
const registerStart = performance.now();
Expand Down
Loading