Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"tldts": "7.4.15",
"use-sync-external-store": "^1.7.0",
"v0": "3.0.7",
"workflow": "^4.8.9",
"workflow": "^5.2.3",
"yaml": "^2.9.1",
"zod": "^4.6.5",
"zustand": "^5.0.15"
Expand Down
21 changes: 16 additions & 5 deletions apps/web/scripts/patch-world-vercel-undici-fetch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* Agent as `dispatcher`. Those are different undici class copies, so
* dispatch() throws "Cannot read private member #P".
*
* Remove the custom dispatcher from world-vercel call sites that ultimately
* reach Node/Next global fetch. For world-local, use npm `undici.fetch` with
* the npm `Agent` so its long-running-step timeout configuration is preserved.
* Remove the custom dispatcher where a Request must reach Node/Next global
* fetch. For string URLs in Workflow 5's shared HTTP core and world-local,
* use npm `undici.fetch` with the npm `Agent` so transport settings survive.
* Idempotent. Safe to skip if the package is absent.
*
* Lives under apps/web. Root .vercelignore must use `/scripts/` (anchored).
Expand All @@ -23,6 +23,8 @@ export function patchSource(src) {
const hasAffectedCallSite =
src.includes('await fetch(') ||
src.includes('await undiciFetch(') ||
src.includes(': fetch(url, {') ||
src.includes(': undiciFetch(url, {') ||
src.includes('dispatcher: getDispatcher(');
if (!hasAffectedCallSite) {
return { src, result: 'no-fetch-call' };
Expand All @@ -31,17 +33,26 @@ export function patchSource(src) {
// Keep global/Next fetch. undici.fetch rejects Request objects
// ("Failed to parse URL from [object Request]") which getRun uses.
// dpl_BBme started (runId) but status poll 500'd after the rename.
if (next.includes('fetch as undiciFetch')) {
if (next.includes('await undiciFetch(')) {
next = next.replace(
/^import \{ fetch as undiciFetch \} from 'undici';\n/m,
'',
);
next = next.replaceAll('await undiciFetch(', 'await fetch(');
}
// Workflow 5's shared HTTP core uses a string URL. Keep its custom Agent
// paired with npm undici instead of passing it to Node/Next global fetch.
if (next.includes(': fetch(url, {') && next.includes('dispatcher,')) {
next = "import { fetch as undiciFetch } from 'undici';\n" + next;
next = next.replace(': fetch(url, {', ': undiciFetch(url, {');
}
// Do not pass Agent across fetch (#P / #dispatch).
next = next
.replace(/\s*dispatcher:\s*getDispatcher\([^)]*\),?/g, '')
.replace(/^\s*dispatcher,\s*$/gm, '');
.replace(/^\s*dispatcher:\s*undiciDispatcher,\s*$/gm, '');
if (!next.includes(': undiciFetch(url, {')) {
next = next.replace(/^\s*dispatcher,\s*$/gm, '');
}
if (next === src) {
return { src, result: 'already-patched' };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ export function createQueue(config) {
}
`;

const WORKFLOW_5_UTILS_FIXTURE = `const response = await fetch(undiciRequest, {
dispatcher: undiciDispatcher,
});
`;

const WORKFLOW_5_HTTP_CORE_FIXTURE = `const pending = nodeAgents
? nodeHttpFetch(url, { method })
: fetch(url, {
method,
dispatcher,
});
`;

const LOCAL_QUEUE_FIXTURE = `import { Agent } from 'undici';
export async function deliver(url, httpAgent) {
return (response = await fetch(createWorkflowUrl(url), {
Expand Down Expand Up @@ -85,6 +98,23 @@ describe('patch-world-vercel-undici-fetch (issue #1538)', () => {
expect(src).not.toMatch(/dispatcher:\s*getDispatcher/);
});

it('keeps Workflow 5 Request inputs on global fetch without a foreign Agent', () => {
const { src, result } = patchSource(WORKFLOW_5_UTILS_FIXTURE);
expect(result).toBe('patched');
expect(src).toContain('await fetch(undiciRequest, {');
expect(src).not.toContain('dispatcher: undiciDispatcher');
expect(patchSource(src)).toEqual({ src, result: 'already-patched' });
});

it('pairs Workflow 5 string URL fetches with their npm undici Agent', () => {
const { src, result } = patchSource(WORKFLOW_5_HTTP_CORE_FIXTURE);
expect(result).toBe('patched');
expect(src).toContain("import { fetch as undiciFetch } from 'undici';");
expect(src).toContain(': undiciFetch(url, {');
expect(src).toMatch(/^\s*dispatcher,\s*$/m);
expect(patchSource(src)).toEqual({ src, result: 'already-patched' });
});

it('pairs world-local fetch with its npm Agent and preserves dispatcher settings', () => {
const { src, result } = patchWorldLocalSource(LOCAL_QUEUE_FIXTURE);
expect(result).toBe('patched');
Expand Down Expand Up @@ -163,10 +193,12 @@ describe('patch-world-vercel-undici-fetch (issue #1538)', () => {
'utf8',
);
expect(utils).toContain('await fetch(');
expect(httpCore).toContain('await fetch(');
expect(httpCore).toContain(': undiciFetch(url, {');
expect(httpCore).toContain("import { fetch as undiciFetch } from 'undici';");
expect(utils).not.toContain('undiciFetch');
expect(utils).not.toMatch(/dispatcher:\s*getDispatcher/);
expect(httpCore).not.toMatch(/^\s*dispatcher,\s*$/m);
expect(httpCore).toMatch(/^\s*dispatcher,\s*$/m);
expect(utils).not.toContain('dispatcher: undiciDispatcher');
expect(queue).not.toMatch(/dispatcher:\s*getDispatcher/);
expect(localQueue).toContain("fetch as undiciFetch } from 'undici'");
// world-local 4.2.x patches fetch(createWorkflowUrl(...)). Newer builds
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/video-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { canonicalYouTubeSource, resolveYouTubeSourceId } from '@/lib/video-sour
import { createHash } from 'node:crypto';
import { reasonEnvelope, reasonEnvelopeJson } from '@/lib/api-reason-envelope';
import { canonicalReviewContent, invalidGroundedSpec, parseGroundedSpec, sourceForGroundedSpec, type GroundedSpecRecord } from '@/lib/grounded-build-spec';
import { start } from 'workflow/api';
import { NextResponse } from 'next/server';
import { resolveVideoUrl } from '@/lib/video-url-request';
import { withWorldVercelFetch } from '@/lib/world-vercel-fetch';
Expand Down Expand Up @@ -262,6 +261,7 @@ const SOURCE_HASH = /^[a-f0-9]{64}$/;
type StartVideoPackExtraction = (identity: VideoPackV0Json) => Promise<void>;

async function startDurableVideoPackExtraction(identity: VideoPackV0Json): Promise<void> {
const { start } = await import('workflow/api');
await withWorldVercelFetch(async () => {
await start(videoPackExtractionWorkflow, [identity]);
});
Expand Down
Loading
Loading