Skip to content

Commit 4440025

Browse files
committed
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](netlify/cli#7700 (comment)) and which I've personally used to unblock myself.
1 parent a98cd8e commit 4440025

File tree

3 files changed

+81
-14
lines changed

3 files changed

+81
-14
lines changed

packages/edge-bundler/node/bridge.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,57 @@ test('Does inherit environment variables if `extendEnv` is not set', async () =>
136136

137137
await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 })
138138
})
139+
140+
test('Provides detailed error message when downloaded binary cannot be executed', async () => {
141+
const tmpDir = await tmp.dir()
142+
const latestVersion = semver.minVersion(DENO_VERSION_RANGE)?.version ?? ''
143+
const data = new PassThrough()
144+
const archive = archiver('zip', { zlib: { level: 9 } })
145+
146+
archive.pipe(data)
147+
// Create a binary that will fail to execute (invalid content)
148+
archive.append(Buffer.from('invalid binary content'), {
149+
name: platform === 'win32' ? 'deno.exe' : 'deno',
150+
})
151+
archive.finalize()
152+
153+
const target = getPlatformTarget()
154+
155+
nock('https://dl.deno.land').get('/release-latest.txt').reply(200, `v${latestVersion}`)
156+
nock('https://dl.deno.land')
157+
.get(`/release/v${latestVersion}/deno-${target}.zip`)
158+
.reply(200, () => data)
159+
160+
const deno = new DenoBridge({
161+
cacheDirectory: tmpDir.path,
162+
useGlobal: false,
163+
})
164+
165+
try {
166+
await deno.getBinaryPath()
167+
expect.fail('Should have thrown an error')
168+
} catch (error) {
169+
expect(error).toBeInstanceOf(Error)
170+
const errorMessage = (error as Error).message
171+
172+
// Verify the error message structure and required information
173+
expect(errorMessage).toContain('Failed to set up Deno for Edge Functions')
174+
175+
// Verify actual error is included (should be ENOEXEC, EACCES, or similar)
176+
expect(errorMessage).toMatch(/Error: .+/)
177+
178+
// Verify binary path is shown with actual path (not just the label)
179+
expect(errorMessage).toMatch(/Downloaded to: .+\/deno(\.exe)?/)
180+
expect(errorMessage).toContain(tmpDir.path)
181+
182+
// Verify platform info is shown with actual values (not just the label)
183+
expect(errorMessage).toMatch(/Platform: (darwin|linux|win32)\/(x64|arm64|ia32)/)
184+
185+
// Verify actionable guidance is provided
186+
expect(errorMessage).toContain('This may be caused by permissions, antivirus software, or platform incompatibility')
187+
expect(errorMessage).toContain('Try clearing the Deno cache and retrying')
188+
expect(errorMessage).toContain('https://ntl.fyi/install-deno')
189+
}
190+
191+
await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 })
192+
})

packages/edge-bundler/node/bridge.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,23 @@ export class DenoBridge {
8282
this.logger.system(`Downloading Deno CLI to ${this.cacheDirectory}`)
8383

8484
const binaryPath = await download(this.cacheDirectory, this.versionRange, this.logger)
85-
const downloadedVersion = await this.getBinaryVersion(binaryPath)
85+
const result = await this.getBinaryVersion(binaryPath)
8686

87-
// We should never get here, because it means that `DENO_VERSION_RANGE` is
88-
// a malformed semver range. If this does happen, let's throw an error so
89-
// that the tests catch it.
90-
if (downloadedVersion === undefined) {
87+
// If we can't execute the downloaded binary, provide detailed error information
88+
// to help users diagnose the issue (permissions, platform incompatibility, etc.)
89+
if (result.error) {
9190
const error = new Error(
92-
'There was a problem setting up the Edge Functions environment. To try a manual installation, visit https://ntl.fyi/install-deno.',
91+
`Failed to set up Deno for Edge Functions.
92+
Error: ${result.error.message}
93+
Downloaded to: ${binaryPath}
94+
Platform: ${process.platform}/${process.arch}
95+
96+
This may be caused by permissions, antivirus software, or platform incompatibility.
97+
98+
Try clearing the Deno cache and retrying:
99+
${this.cacheDirectory}
100+
101+
To install Deno manually: https://ntl.fyi/install-deno`,
93102
)
94103

95104
await this.onAfterDownload?.(error)
@@ -99,26 +108,29 @@ export class DenoBridge {
99108
throw error
100109
}
101110

102-
await this.writeVersionFile(downloadedVersion)
111+
await this.writeVersionFile(result.version)
103112

104113
await this.onAfterDownload?.()
105114

106115
return binaryPath
107116
}
108117

109-
async getBinaryVersion(binaryPath: string) {
118+
async getBinaryVersion(
119+
binaryPath: string,
120+
): Promise<{ version: string; error?: undefined } | { version?: undefined; error: Error }> {
110121
try {
111122
const { stdout } = await execa(binaryPath, ['--version'])
112123
const version = stdout.match(/^deno ([\d.]+)/)
113124

114125
if (!version) {
115126
this.logger.system(`getBinaryVersion no version found. binaryPath ${binaryPath}`)
116-
return
127+
return { error: new Error('Could not parse Deno version from output') }
117128
}
118129

119-
return version[1]
130+
return { version: version[1] }
120131
} catch (error) {
121132
this.logger.system('getBinaryVersion failed', error)
133+
return { error: error instanceof Error ? error : new Error(String(error)) }
122134
}
123135
}
124136

@@ -150,11 +162,11 @@ export class DenoBridge {
150162
}
151163

152164
const globalBinaryName = 'deno'
153-
const globalVersion = await this.getBinaryVersion(globalBinaryName)
165+
const result = await this.getBinaryVersion(globalBinaryName)
154166

155-
if (globalVersion === undefined || !semver.satisfies(globalVersion, this.versionRange)) {
167+
if (result.error || !semver.satisfies(result.version, this.versionRange)) {
156168
this.logger.system(
157-
`No globalVersion or semver not satisfied. globalVersion: ${globalVersion}, versionRange: ${this.versionRange}`,
169+
`No globalVersion or semver not satisfied. globalVersion: ${result.version}, versionRange: ${this.versionRange}`,
158170
)
159171
return
160172
}

packages/edge-bundler/node/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ export const getFunctionConfig = async ({
9797
const collector = await tmp.file()
9898

9999
// Retrieving the version of Deno.
100-
const version = new SemVer((await deno.getBinaryVersion((await deno.getBinaryPath({ silent: true })).path)) || '')
100+
const result = await deno.getBinaryVersion((await deno.getBinaryPath({ silent: true })).path)
101+
const version = new SemVer(result.version || '')
101102

102103
// The extractor will use its exit code to signal different error scenarios,
103104
// based on the list of exit codes we send as an argument. We then capture

0 commit comments

Comments
 (0)