mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
fix bad socket file location (#2021)
Co-authored-by: JJ Kasper <jj@jjsweb.site>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@workflow/next": patch
|
||||
---
|
||||
|
||||
Move `workflow-socket.json` out of `.next/cache/` so it isn't preserved across Vercel/Turborepo builds, and clean up stale copies at builder boot. Resolves `ECONNREFUSED 127.0.0.1:<port>` failures from the webpack loader when a prior build's socket-info file was restored from build cache. The loader now also annotates connection errors with the port, credentials source, and the file being processed.
|
||||
Vendored
+8
@@ -1,7 +1,15 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.biome": "explicit",
|
||||
"source.fixAll.biome": "explicit"
|
||||
},
|
||||
"[typescript]": { "editor.defaultFormatter": "biomejs.biome" },
|
||||
"[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
|
||||
"[javascript]": { "editor.defaultFormatter": "biomejs.biome" },
|
||||
"[javascriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
|
||||
"[json]": { "editor.defaultFormatter": "biomejs.biome" },
|
||||
"[jsonc]": { "editor.defaultFormatter": "biomejs.biome" },
|
||||
"rust-analyzer.linkedProjects": ["packages/swc-plugin-workflow/Cargo.toml"]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ import {
|
||||
resolve,
|
||||
} from 'node:path';
|
||||
import {
|
||||
cleanupStaleSocketInfoFiles,
|
||||
createSocketServer,
|
||||
SOCKET_INFO_FILENAME,
|
||||
type SocketIO,
|
||||
type SocketServerConfig,
|
||||
} from './socket-server.js';
|
||||
@@ -617,13 +619,13 @@ export async function getNextBuilderDeferred() {
|
||||
join(workflowGeneratedDir, 'manifest.json'),
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
staleArtifactPaths.map((stalePath) =>
|
||||
rm(stalePath, { recursive: true, force: true })
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
...staleArtifactPaths.map((stalePath) =>
|
||||
rm(stalePath, { recursive: true, force: true })
|
||||
),
|
||||
cleanupStaleSocketInfoFiles(
|
||||
join(this.config.workingDir, this.getDistDir())
|
||||
),
|
||||
this.removeStaleDeferredTempFiles(flowRouteDir),
|
||||
this.removeStaleDeferredTempFiles(webhookRouteDir),
|
||||
]);
|
||||
@@ -754,8 +756,7 @@ export async function getNextBuilderDeferred() {
|
||||
return join(
|
||||
this.config.workingDir,
|
||||
this.getDistDir(),
|
||||
'cache',
|
||||
'workflow-socket.json'
|
||||
SOCKET_INFO_FILENAME
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { access, copyFile, mkdir, stat, writeFile } from 'node:fs/promises';
|
||||
import { extname, join, resolve } from 'node:path';
|
||||
import type { WorkflowManifest } from '@workflow/builders';
|
||||
import Watchpack from 'watchpack';
|
||||
import { cleanupStaleSocketInfoFiles } from './socket-server.js';
|
||||
|
||||
let CachedNextBuilderEager: any;
|
||||
|
||||
@@ -24,6 +25,16 @@ export async function getNextBuilderEager() {
|
||||
|
||||
class NextBuilder extends BaseBuilderClass {
|
||||
async build() {
|
||||
// Eager mode never starts a discovery socket server, so any leftover
|
||||
// workflow-socket.json is from a previous deferred-mode build and
|
||||
// would make the webpack loader connect to a dead port.
|
||||
await cleanupStaleSocketInfoFiles(
|
||||
join(
|
||||
this.config.workingDir,
|
||||
(this.config as { distDir?: string }).distDir || '.next'
|
||||
)
|
||||
);
|
||||
|
||||
const outputDir = await this.findAppDirectory();
|
||||
const workflowGeneratedDir = join(outputDir, '.well-known/workflow/v1');
|
||||
|
||||
|
||||
+50
-12
@@ -5,6 +5,7 @@ import { dirname, join, relative } from 'node:path';
|
||||
import { transform } from '@swc/core';
|
||||
import {
|
||||
parseMessage,
|
||||
SOCKET_INFO_FILENAME,
|
||||
type SocketMessage,
|
||||
serializeMessage,
|
||||
} from './socket-server.js';
|
||||
@@ -46,6 +47,39 @@ type SocketCredentials = {
|
||||
authToken: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrap a TCP connect failure with context about where the connection was
|
||||
* attempted, where the credentials came from, and which source file was
|
||||
* being processed when it failed. ECONNREFUSED is the common case here, and
|
||||
* the message points the user at the most likely root cause (stale
|
||||
* socket-info file).
|
||||
*/
|
||||
function annotateConnectionError(
|
||||
originalError: unknown,
|
||||
credentials: SocketCredentials
|
||||
): Error {
|
||||
const errorCode =
|
||||
originalError instanceof Error &&
|
||||
'code' in originalError &&
|
||||
typeof (originalError as { code?: unknown }).code === 'string'
|
||||
? ((originalError as { code: string }).code as string)
|
||||
: undefined;
|
||||
const errorMessage =
|
||||
originalError instanceof Error
|
||||
? originalError.message
|
||||
: String(originalError);
|
||||
|
||||
const lines = [
|
||||
`Workflow discovery socket connect failed: ${errorCode ?? errorMessage} (127.0.0.1:${credentials.port})`,
|
||||
];
|
||||
|
||||
const annotated = new Error(lines.join('\n'));
|
||||
if (originalError instanceof Error) {
|
||||
(annotated as { cause?: unknown }).cause = originalError;
|
||||
}
|
||||
return annotated;
|
||||
}
|
||||
|
||||
const ROUTE_STUB_FILE_MARKER = 'WORKFLOW_ROUTE_STUB_FILE';
|
||||
const ROUTE_STUB_BUILD_WAIT_TIMEOUT_MS = 120_000;
|
||||
let pendingDeferredRouteStubBuildPromise: Promise<void> | null = null;
|
||||
@@ -174,19 +208,13 @@ function getSocketInfoFilePath(): string | null {
|
||||
// Fallback for worker processes that don't inherit dynamic env updates
|
||||
// from the process that created the socket server.
|
||||
const distDir = process.env.WORKFLOW_NEXT_DIST_DIR || '.next';
|
||||
const cwdFallbackPath = join(
|
||||
process.cwd(),
|
||||
distDir,
|
||||
'cache',
|
||||
'workflow-socket.json'
|
||||
);
|
||||
const cwdFallbackPath = join(process.cwd(), distDir, SOCKET_INFO_FILENAME);
|
||||
const projectRoot = process.env.WORKFLOW_PROJECT_ROOT;
|
||||
if (projectRoot) {
|
||||
const projectRootFallbackPath = join(
|
||||
projectRoot,
|
||||
distDir,
|
||||
'cache',
|
||||
'workflow-socket.json'
|
||||
SOCKET_INFO_FILENAME
|
||||
);
|
||||
if (existsSync(projectRootFallbackPath)) {
|
||||
return projectRootFallbackPath;
|
||||
@@ -287,12 +315,17 @@ async function getSocketClient(): Promise<Socket | null> {
|
||||
};
|
||||
const onError = (error: Error) => {
|
||||
cleanup();
|
||||
reject(error);
|
||||
reject(annotateConnectionError(error, socketCredentials));
|
||||
};
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup();
|
||||
socket.destroy();
|
||||
reject(new Error('Socket connection timeout'));
|
||||
reject(
|
||||
annotateConnectionError(
|
||||
new Error('Socket connection timeout'),
|
||||
socketCredentials
|
||||
)
|
||||
);
|
||||
}, 1000);
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout);
|
||||
@@ -379,7 +412,12 @@ async function createSocketConnection(
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup();
|
||||
socket.destroy();
|
||||
reject(new Error('Socket connection timeout'));
|
||||
reject(
|
||||
annotateConnectionError(
|
||||
new Error('Socket connection timeout'),
|
||||
socketCredentials
|
||||
)
|
||||
);
|
||||
}, timeoutMs);
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout);
|
||||
@@ -394,7 +432,7 @@ async function createSocketConnection(
|
||||
const onError = (error: Error) => {
|
||||
cleanup();
|
||||
socket.destroy();
|
||||
reject(error);
|
||||
reject(annotateConnectionError(error, socketCredentials));
|
||||
};
|
||||
|
||||
socket.on('connect', onConnect);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { createServer, type Server, type Socket } from 'node:net';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
@@ -54,8 +54,41 @@ export interface SocketIO {
|
||||
getAuthToken(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename for the socket-info file.
|
||||
*/
|
||||
export const SOCKET_INFO_FILENAME = 'workflow-socket.json';
|
||||
|
||||
/**
|
||||
* Previous filesystem location for the socket-info file. This file lives
|
||||
* inside `.next/cache/`, which Vercel and Turborepo preserve across builds —
|
||||
* a stale file from a prior build would cause the loader to attempt to
|
||||
* connect to a dead port (ECONNREFUSED). The current location is a sibling
|
||||
* of `cache/` so it isn't preserved.
|
||||
*
|
||||
* Exported so the builders can unlink the legacy path at boot, cleaning up
|
||||
* any leftover file written by older versions of the SDK.
|
||||
*/
|
||||
export const LEGACY_SOCKET_INFO_RELATIVE_PATH = join(
|
||||
'cache',
|
||||
SOCKET_INFO_FILENAME
|
||||
);
|
||||
|
||||
function getDefaultSocketInfoFilePath(): string {
|
||||
return join(process.cwd(), '.next', 'cache', 'workflow-socket.json');
|
||||
return join(process.cwd(), '.next', SOCKET_INFO_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any stale socket-info files at boot.
|
||||
* @param distDir absolute path to the project's `.next` directory.
|
||||
*/
|
||||
export async function cleanupStaleSocketInfoFiles(
|
||||
distDir: string
|
||||
): Promise<void> {
|
||||
await Promise.all([
|
||||
rm(join(distDir, SOCKET_INFO_FILENAME), { force: true }),
|
||||
rm(join(distDir, LEGACY_SOCKET_INFO_RELATIVE_PATH), { force: true }),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user