From 3f7574a43c851b6328f4495eeb6252dbacd32f64 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Wed, 15 Oct 2025 19:07:58 -0400 Subject: [PATCH] fix: throw precise, actionable error on unusable deno cli ``` Error: There was a problem setting up the Edge Functions environment. To try a manual installation, visit https://ntl.fyi/install-deno. ``` This happens _a lot_: https://github.com/netlify/cli/issues?q=is%3Aissue%20%22There%20was%20a%20problem%20setting%20up%20the%20Edge%20Functions%20environment%22. It means we failed to download, install, or execute a local deno binary, or we're trying to use one that doesn't behave as expected or match the expected version range. This error message is imprecise and not very actionable. This commit bubbles the previously swallowed error details, adds more info, and adds a recommended step that [has been found to unblock users](https://github.com/netlify/cli/issues/7700#issuecomment-3357911906) and which I've personally used to unblock myself. --- packages/edge-bundler/node/bridge.test.ts | 45 +++++++++++++++++++++++ packages/edge-bundler/node/bridge.ts | 39 +++++++++++++------- packages/edge-bundler/node/config.ts | 3 +- 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/packages/edge-bundler/node/bridge.test.ts b/packages/edge-bundler/node/bridge.test.ts index 6c973cebdc..8e51893be1 100644 --- a/packages/edge-bundler/node/bridge.test.ts +++ b/packages/edge-bundler/node/bridge.test.ts @@ -136,3 +136,48 @@ test('Does inherit environment variables if `extendEnv` is not set', async () => await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 }) }) + +test('Provides actionable error message when downloaded binary cannot be executed', async () => { + const tmpDir = await tmp.dir() + const latestVersion = semver.minVersion(DENO_VERSION_RANGE)?.version ?? '' + const data = new PassThrough() + const archive = archiver('zip', { zlib: { level: 9 } }) + + archive.pipe(data) + // Create a binary that will fail to execute (invalid content) + archive.append(Buffer.from('invalid binary content'), { + name: platform === 'win32' ? 'deno.exe' : 'deno', + }) + archive.finalize() + + const target = getPlatformTarget() + + nock('https://dl.deno.land').get('/release-latest.txt').reply(200, `v${latestVersion}`) + nock('https://dl.deno.land') + .get(`/release/v${latestVersion}/deno-${target}.zip`) + .reply(200, () => data) + + const deno = new DenoBridge({ + cacheDirectory: tmpDir.path, + useGlobal: false, + }) + + try { + await deno.getBinaryPath() + expect.fail('Should have thrown an error') + } catch (error) { + expect(error).toBeInstanceOf(Error) + const errorMessage = (error as Error).message + + expect(errorMessage).toContain('Failed to set up Deno for Edge Functions') + expect(errorMessage).toMatch(/Error:/) + expect(errorMessage).toMatch(/Downloaded to: .+deno(\.exe)?/) + expect(errorMessage).toContain(tmpDir.path) + expect(errorMessage).toMatch(/Platform: (darwin|linux|win32)\/(x64|arm64|ia32)/) + expect(errorMessage).toContain('This may be caused by permissions, antivirus software, or platform incompatibility') + expect(errorMessage).toContain('Try clearing the Deno cache directory and retrying') + expect(errorMessage).toContain('https://ntl.fyi/install-deno') + } + + await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 }) +}) diff --git a/packages/edge-bundler/node/bridge.ts b/packages/edge-bundler/node/bridge.ts index 85fb386d1f..78e4954880 100644 --- a/packages/edge-bundler/node/bridge.ts +++ b/packages/edge-bundler/node/bridge.ts @@ -82,14 +82,24 @@ export class DenoBridge { this.logger.system(`Downloading Deno CLI to ${this.cacheDirectory}`) const binaryPath = await download(this.cacheDirectory, this.versionRange, this.logger) - const downloadedVersion = await this.getBinaryVersion(binaryPath) + const result = await this.getBinaryVersion(binaryPath) - // We should never get here, because it means that `DENO_VERSION_RANGE` is - // a malformed semver range. If this does happen, let's throw an error so - // that the tests catch it. - if (downloadedVersion === undefined) { + // If we can't execute the downloaded binary, provide actionable info to diagnose and self-heal if possible + if (result.error) { const error = new Error( - 'There was a problem setting up the Edge Functions environment. To try a manual installation, visit https://ntl.fyi/install-deno.', + `Failed to set up Deno for Edge Functions. +Error: ${result.error.message} +Downloaded to: ${binaryPath} +Platform: ${process.platform}/${process.arch} + +This may be caused by permissions, antivirus software, or platform incompatibility. + +Try clearing the Deno cache directory and retrying: + ${this.cacheDirectory} + +Supported Deno versions: ${this.versionRange} + +To install Deno manually: https://ntl.fyi/install-deno`, ) await this.onAfterDownload?.(error) @@ -99,26 +109,29 @@ export class DenoBridge { throw error } - await this.writeVersionFile(downloadedVersion) + await this.writeVersionFile(result.version) await this.onAfterDownload?.() return binaryPath } - async getBinaryVersion(binaryPath: string) { + async getBinaryVersion( + binaryPath: string, + ): Promise<{ version: string; error?: undefined } | { version?: undefined; error: Error }> { try { const { stdout } = await execa(binaryPath, ['--version']) const version = stdout.match(/^deno ([\d.]+)/) if (!version) { this.logger.system(`getBinaryVersion no version found. binaryPath ${binaryPath}`) - return + return { error: new Error('Could not parse Deno version from output') } } - return version[1] + return { version: version[1] } } catch (error) { this.logger.system('getBinaryVersion failed', error) + return { error: error instanceof Error ? error : new Error(String(error)) } } } @@ -150,11 +163,11 @@ export class DenoBridge { } const globalBinaryName = 'deno' - const globalVersion = await this.getBinaryVersion(globalBinaryName) + const result = await this.getBinaryVersion(globalBinaryName) - if (globalVersion === undefined || !semver.satisfies(globalVersion, this.versionRange)) { + if (result.error || !semver.satisfies(result.version, this.versionRange)) { this.logger.system( - `No globalVersion or semver not satisfied. globalVersion: ${globalVersion}, versionRange: ${this.versionRange}`, + `No globalVersion or semver not satisfied. globalVersion: ${result.version}, versionRange: ${this.versionRange}`, ) return } diff --git a/packages/edge-bundler/node/config.ts b/packages/edge-bundler/node/config.ts index df76405915..c6a3b8777f 100644 --- a/packages/edge-bundler/node/config.ts +++ b/packages/edge-bundler/node/config.ts @@ -97,7 +97,8 @@ export const getFunctionConfig = async ({ const collector = await tmp.file() // Retrieving the version of Deno. - const version = new SemVer((await deno.getBinaryVersion((await deno.getBinaryPath({ silent: true })).path)) || '') + const result = await deno.getBinaryVersion((await deno.getBinaryPath({ silent: true })).path) + const version = new SemVer(result.version || '') // The extractor will use its exit code to signal different error scenarios, // based on the list of exit codes we send as an argument. We then capture