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 3d252f4efb..34824bc5d2 100644 --- a/packages/edge-bundler/node/bridge.ts +++ b/packages/edge-bundler/node/bridge.ts @@ -84,14 +84,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) @@ -101,26 +111,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)) } } } @@ -152,11 +165,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 94e06ac37e..b73fe3fc75 100644 --- a/packages/edge-bundler/node/config.ts +++ b/packages/edge-bundler/node/config.ts @@ -96,7 +96,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