From 96a27f3d8a250c995907773d1aa695f80d43d9d0 Mon Sep 17 00:00:00 2001 From: MrBBot Date: Thu, 11 Jan 2024 18:53:50 +0000 Subject: [PATCH] [wrangler] fix: sanitise error reports and only report unknown errors (#4707) * Revert "chore: temporarily disable sentry error reporting (#4701)" This reverts commit 1c9817b4663f4f8c28e1c998949067db5aef977e. * fix: sanitise sentry events Removes PII information from Sentry events * chore: annotate user errors Introduces a new `UserError` base class. Instances of this class won't be reported to Sentry. * fix: only report non-user errors * test: ensure only non-user errors reported and PII not included --- .changeset/calm-falcons-reply.md | 7 + .../create-pullrequest-prerelease.yml | 1 + .github/workflows/prereleases.yml | 2 + .github/workflows/release.yml | 1 + .../wrangler/src/__tests__/deploy.test.ts | 6 +- .../wrangler/src/__tests__/sentry.test.ts | 275 ++++++++++++++++-- packages/wrangler/src/api/mtls-certificate.ts | 5 +- packages/wrangler/src/cfetch/index.ts | 4 +- packages/wrangler/src/cfetch/internal.ts | 7 +- packages/wrangler/src/config/index.ts | 3 +- .../src/constellation/createProject.tsx | 3 +- .../src/constellation/uploadModel.tsx | 3 +- packages/wrangler/src/constellation/utils.ts | 5 +- packages/wrangler/src/d1/create.tsx | 5 +- packages/wrangler/src/d1/execute.tsx | 15 +- packages/wrangler/src/d1/migrations/apply.tsx | 5 +- .../wrangler/src/d1/migrations/create.tsx | 3 +- .../wrangler/src/d1/migrations/helpers.ts | 3 +- packages/wrangler/src/d1/migrations/list.tsx | 3 +- .../wrangler/src/d1/timeTravel/restore.ts | 5 +- packages/wrangler/src/d1/timeTravel/utils.ts | 9 +- packages/wrangler/src/d1/trimmer.ts | 4 +- packages/wrangler/src/d1/utils.ts | 3 +- packages/wrangler/src/delete.ts | 11 +- packages/wrangler/src/deploy/deploy.ts | 32 +- packages/wrangler/src/deploy/index.ts | 9 +- .../wrangler/src/deployment-bundle/bundle.ts | 7 +- .../wrangler/src/deployment-bundle/capnp.ts | 3 +- .../wrangler/src/deployment-bundle/entry.ts | 5 +- .../find-additional-modules.ts | 3 +- .../deployment-bundle/guess-worker-format.ts | 5 +- .../deployment-bundle/module-collection.ts | 3 +- .../src/deployment-bundle/run-custom-build.ts | 3 +- packages/wrangler/src/deployments.ts | 9 +- packages/wrangler/src/dev.tsx | 27 +- packages/wrangler/src/dev/remote.tsx | 21 +- .../wrangler/src/dev/validate-dev-props.ts | 11 +- packages/wrangler/src/dialogs.ts | 3 +- packages/wrangler/src/errors.ts | 19 +- packages/wrangler/src/generate/index.ts | 5 +- packages/wrangler/src/git-client.ts | 7 +- packages/wrangler/src/index.ts | 38 ++- packages/wrangler/src/init.ts | 3 +- packages/wrangler/src/kv/helpers.ts | 17 +- packages/wrangler/src/kv/index.ts | 9 +- packages/wrangler/src/package-manager.ts | 3 +- packages/wrangler/src/pages/build.ts | 4 +- .../wrangler/src/pages/deployment-tails.ts | 2 +- packages/wrangler/src/pages/dev.ts | 5 +- packages/wrangler/src/pages/errors.ts | 5 +- .../wrangler/src/pages/functions/routes.ts | 5 +- packages/wrangler/src/parse.ts | 19 +- packages/wrangler/src/r2/helpers.ts | 3 +- packages/wrangler/src/r2/index.ts | 4 +- packages/wrangler/src/routes.ts | 5 +- packages/wrangler/src/secret/index.ts | 11 +- packages/wrangler/src/sentry/index.ts | 33 +++ packages/wrangler/src/sites.ts | 5 +- packages/wrangler/src/tail/filters.ts | 4 +- packages/wrangler/src/tail/index.ts | 3 +- packages/wrangler/src/type-generation.ts | 3 +- packages/wrangler/src/user/access.ts | 3 +- packages/wrangler/src/user/choose-account.tsx | 5 +- packages/wrangler/src/user/user.ts | 15 +- packages/wrangler/src/zones.ts | 9 +- 65 files changed, 587 insertions(+), 186 deletions(-) create mode 100644 .changeset/calm-falcons-reply.md diff --git a/.changeset/calm-falcons-reply.md b/.changeset/calm-falcons-reply.md new file mode 100644 index 000000000000..9bd4477ffc4b --- /dev/null +++ b/.changeset/calm-falcons-reply.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +fix: only offer to report unknown errors + +Previously, Wrangler would offer to report any error to Cloudflare. This included errors caused by misconfigurations or invalid commands. This change ensures those types of errors aren't reported. diff --git a/.github/workflows/create-pullrequest-prerelease.yml b/.github/workflows/create-pullrequest-prerelease.yml index 0075e383804e..c15e2db5da30 100644 --- a/.github/workflows/create-pullrequest-prerelease.yml +++ b/.github/workflows/create-pullrequest-prerelease.yml @@ -68,6 +68,7 @@ jobs: NODE_ENV: "production" ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} ALGOLIA_PUBLIC_KEY: ${{ secrets.ALGOLIA_PUBLIC_KEY }} + SENTRY_DSN: "https://9edbb8417b284aa2bbead9b4c318918b@sentry10.cfdata.org/583" CI_OS: ${{ runner.os }} - name: Pack miniflare diff --git a/.github/workflows/prereleases.yml b/.github/workflows/prereleases.yml index ebf660851c2a..3a920be6ac67 100644 --- a/.github/workflows/prereleases.yml +++ b/.github/workflows/prereleases.yml @@ -67,6 +67,7 @@ jobs: # this is the "test/staging" key for sparrow analytics SPARROW_SOURCE_KEY: "5adf183f94b3436ba78d67f506965998" ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} + SENTRY_DSN: "https://9edbb8417b284aa2bbead9b4c318918b@sentry10.cfdata.org/583" ALGOLIA_PUBLIC_KEY: ${{ secrets.ALGOLIA_PUBLIC_KEY }} working-directory: packages/wrangler @@ -111,6 +112,7 @@ jobs: NODE_ENV: "production" ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} ALGOLIA_PUBLIC_KEY: ${{ secrets.ALGOLIA_PUBLIC_KEY }} + SENTRY_DSN: "https://9edbb8417b284aa2bbead9b4c318918b@sentry10.cfdata.org/583" CI_OS: ${{ runner.os }} - name: Build & Publish Prerelease Registry diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 662c7d754904..0d00aab997a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,6 +62,7 @@ jobs: NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} ALGOLIA_PUBLIC_KEY: ${{ secrets.ALGOLIA_PUBLIC_KEY }} + SENTRY_DSN: "https://9edbb8417b284aa2bbead9b4c318918b@sentry10.cfdata.org/583" NODE_ENV: "production" # This is the "production" key for sparrow analytics. diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts index 363ab62b41eb..892b1e196777 100644 --- a/packages/wrangler/src/__tests__/deploy.test.ts +++ b/packages/wrangler/src/__tests__/deploy.test.ts @@ -8109,7 +8109,7 @@ export default{ }); await expect(runWrangler("deploy")).rejects.toMatchInlineSnapshot( - `[ParseError: A request to the Cloudflare API (/accounts/some-account-id/workers/scripts/test-name) failed.]` + `[APIError: A request to the Cloudflare API (/accounts/some-account-id/workers/scripts/test-name) failed.]` ); expect(std).toMatchInlineSnapshot(` Object { @@ -8182,7 +8182,7 @@ export default{ }); await expect(runWrangler("deploy")).rejects.toMatchInlineSnapshot( - `[ParseError: A request to the Cloudflare API (/accounts/some-account-id/workers/scripts/test-name) failed.]` + `[APIError: A request to the Cloudflare API (/accounts/some-account-id/workers/scripts/test-name) failed.]` ); expect(std).toMatchInlineSnapshot(` @@ -8252,7 +8252,7 @@ export default{ }); await expect(runWrangler("deploy")).rejects.toMatchInlineSnapshot( - `[ParseError: A request to the Cloudflare API (/accounts/some-account-id/workers/scripts/test-name) failed.]` + `[APIError: A request to the Cloudflare API (/accounts/some-account-id/workers/scripts/test-name) failed.]` ); expect(std).toMatchInlineSnapshot(` Object { diff --git a/packages/wrangler/src/__tests__/sentry.test.ts b/packages/wrangler/src/__tests__/sentry.test.ts index 706b5227fd00..9d0d132804f1 100644 --- a/packages/wrangler/src/__tests__/sentry.test.ts +++ b/packages/wrangler/src/__tests__/sentry.test.ts @@ -1,3 +1,5 @@ +import assert from "node:assert"; +import * as Sentry from "@sentry/node"; import { rest } from "msw"; import { mockAccountId, mockApiToken } from "./helpers/mock-account-id"; @@ -10,6 +12,11 @@ import { runWrangler } from "./helpers/run-wrangler"; declare const global: { SENTRY_DSN: string | undefined }; +interface EnvelopeRequest { + envelope: string; + url: string; +} + describe("sentry", () => { const ORIGINAL_SENTRY_DSN = global.SENTRY_DSN; const std = mockConsoleMethods(); @@ -18,13 +25,14 @@ describe("sentry", () => { mockApiToken(); const { setIsTTY } = useMockIsTTY(); - let sentryRequests: { count: number } | undefined; + let sentryRequests: EnvelopeRequest[] | undefined; beforeEach(() => { global.SENTRY_DSN = "https://9edbb8417b284aa2bbead9b4c318918b@sentry.example.com/24601"; sentryRequests = mockSentryEndpoint(); + Sentry.getCurrentScope().clearBreadcrumbs(); }); afterEach(() => { global.SENTRY_DSN = ORIGINAL_SENTRY_DSN; @@ -34,20 +42,22 @@ describe("sentry", () => { describe("non interactive", () => { it("should not hit sentry in normal usage", async () => { await runWrangler("version"); - expect(sentryRequests?.count).toEqual(0); + expect(sentryRequests?.length).toEqual(0); }); it("should not hit sentry after error", async () => { - await expect(runWrangler("delete")).rejects.toMatchInlineSnapshot( - `[AssertionError: A worker name must be defined, either via --name, or in wrangler.toml]` - ); + await expect(runWrangler("whoami")).rejects.toMatchInlineSnapshot(` + [FetchError: request to https://api.cloudflare.com/client/v4/user failed, reason: No mock found for GET https://api.cloudflare.com/client/v4/user + ] + `); expect(std.out).toMatchInlineSnapshot(` - " - If you think this is a bug then please create an issue at https://github.com/cloudflare/workers-sdk/issues/new/choose - ? Would you like to report this error to Cloudflare? - 🤖 Using fallback value in non-interactive context: no" - `); - expect(sentryRequests?.count).toEqual(0); + "Getting User settings... + + If you think this is a bug then please create an issue at https://github.com/cloudflare/workers-sdk/issues/new/choose + ? Would you like to report this error to Cloudflare? + 🤖 Using fallback value in non-interactive context: no" + `); + expect(sentryRequests?.length).toEqual(0); }); }); describe("interactive", () => { @@ -57,49 +67,262 @@ describe("sentry", () => { afterEach(() => { setIsTTY(false); }); + it("should not hit sentry in normal usage", async () => { await runWrangler("version"); - expect(sentryRequests?.count).toEqual(0); + expect(sentryRequests?.length).toEqual(0); }); - it("should not hit sentry after error when permission denied", async () => { + + it("should not hit sentry with user error", async () => { + await expect(runWrangler("delete")).rejects.toMatchInlineSnapshot( + `[Error: A worker name must be defined, either via --name, or in wrangler.toml]` + ); + expect(std.out).toMatchInlineSnapshot(` + " + If you think this is a bug then please create an issue at https://github.com/cloudflare/workers-sdk/issues/new/choose" + `); + expect(sentryRequests?.length).toEqual(0); + }); + + it("should not hit sentry after reportable error when permission denied", async () => { mockConfirm({ text: "Would you like to report this error to Cloudflare?", result: false, }); - await expect(runWrangler("delete")).rejects.toMatchInlineSnapshot( - `[AssertionError: A worker name must be defined, either via --name, or in wrangler.toml]` - ); + await expect(runWrangler("whoami")).rejects.toMatchInlineSnapshot(` + [FetchError: request to https://api.cloudflare.com/client/v4/user failed, reason: No mock found for GET https://api.cloudflare.com/client/v4/user + ] + `); expect(std.out).toMatchInlineSnapshot(` - " + "Getting User settings... + If you think this is a bug then please create an issue at https://github.com/cloudflare/workers-sdk/issues/new/choose" `); - expect(sentryRequests?.count).toEqual(0); + expect(sentryRequests?.length).toEqual(0); }); - it("should hit sentry after error when permission provided", async () => { + + it("should hit sentry after reportable error when permission provided", async () => { mockConfirm({ text: "Would you like to report this error to Cloudflare?", result: true, }); - await expect(runWrangler("delete")).rejects.toMatchInlineSnapshot( - `[AssertionError: A worker name must be defined, either via --name, or in wrangler.toml]` - ); + await expect(runWrangler("whoami")).rejects.toMatchInlineSnapshot(` + [FetchError: request to https://api.cloudflare.com/client/v4/user failed, reason: No mock found for GET https://api.cloudflare.com/client/v4/user + ] + `); expect(std.out).toMatchInlineSnapshot(` - " + "Getting User settings... + If you think this is a bug then please create an issue at https://github.com/cloudflare/workers-sdk/issues/new/choose" `); + // Sentry sends multiple HTTP requests to capture breadcrumbs - expect(sentryRequests?.count).toBeGreaterThan(0); + expect(sentryRequests?.length).toBeGreaterThan(0); + assert(sentryRequests !== undefined); + + // Check requests don't include PII + const envelopes = sentryRequests.map(({ envelope }) => { + const parts = envelope.split("\n").map((line) => JSON.parse(line)); + expect(parts).toHaveLength(3); + return { header: parts[0], type: parts[1], data: parts[2] }; + }); + const event = envelopes.find(({ type }) => type.type === "event"); + assert(event !== undefined); + + // Redact fields with random contents we know don't contain PII + event.header.event_id = ""; + event.header.sent_at = ""; + event.header.trace.trace_id = ""; + event.header.trace.release = ""; + for (const exception of event.data.exception.values) { + for (const frame of exception.stacktrace.frames) { + if ( + frame.filename.startsWith("C:\\Project\\") || + frame.filename.startsWith("/project/") + ) { + frame.filename = "/project/..."; + } + frame.function = ""; + frame.lineno = 0; + frame.colno = 0; + frame.in_app = false; + frame.pre_context = []; + frame.context_line = ""; + frame.post_context = []; + } + } + event.data.event_id = ""; + event.data.contexts.trace.trace_id = ""; + event.data.contexts.trace.span_id = ""; + event.data.contexts.runtime.version = ""; + event.data.contexts.app.app_start_time = ""; + event.data.contexts.app.app_memory = 0; + event.data.contexts.os = {}; + event.data.contexts.device = {}; + event.data.timestamp = 0; + event.data.release = ""; + for (const breadcrumb of event.data.breadcrumbs) { + breadcrumb.timestamp = 0; + } + + // If more data is included in the Sentry request, we'll need to verify it + // couldn't contain PII and update this snapshot + expect(event).toMatchInlineSnapshot(` + Object { + "data": Object { + "breadcrumbs": Array [ + Object { + "level": "log", + "message": "wrangler whoami", + "timestamp": 0, + }, + ], + "contexts": Object { + "app": Object { + "app_memory": 0, + "app_start_time": "", + }, + "cloud_resource": Object {}, + "device": Object {}, + "os": Object {}, + "runtime": Object { + "name": "node", + "version": "", + }, + "trace": Object { + "span_id": "", + "trace_id": "", + }, + }, + "environment": "production", + "event_id": "", + "exception": Object { + "values": Array [ + Object { + "mechanism": Object { + "handled": true, + "type": "generic", + }, + "stacktrace": Object { + "frames": Array [ + Object { + "colno": 0, + "context_line": "", + "filename": "/project/...", + "function": "", + "in_app": false, + "lineno": 0, + "module": "@mswjs.interceptors.src.interceptors.ClientRequest:NodeClientRequest.ts", + "post_context": Array [], + "pre_context": Array [], + }, + Object { + "colno": 0, + "context_line": "", + "filename": "/project/...", + "function": "", + "in_app": false, + "lineno": 0, + "module": "@mswjs.interceptors.src.interceptors.ClientRequest:NodeClientRequest.ts", + "post_context": Array [], + "pre_context": Array [], + }, + Object { + "colno": 0, + "context_line": "", + "filename": "node:domain", + "function": "", + "in_app": false, + "lineno": 0, + "module": "node:domain", + "post_context": Array [], + "pre_context": Array [], + }, + Object { + "colno": 0, + "context_line": "", + "filename": "node:events", + "function": "", + "in_app": false, + "lineno": 0, + "module": "node:events", + "post_context": Array [], + "pre_context": Array [], + }, + Object { + "colno": 0, + "context_line": "", + "filename": "/project/...", + "function": "", + "in_app": false, + "lineno": 0, + "module": "node-fetch.lib:index", + "post_context": Array [], + "pre_context": Array [], + }, + ], + }, + "type": "FetchError", + "value": "request to https://api.cloudflare.com/client/v4/user failed, reason: No mock found for GET https://api.cloudflare.com/client/v4/user + ", + }, + ], + }, + "modules": Object {}, + "platform": "node", + "release": "", + "sdk": Object { + "integrations": Array [ + "InboundFilters", + "FunctionToString", + "LinkedErrors", + "OnUncaughtException", + "OnUnhandledRejection", + "ContextLines", + "Context", + "Modules", + ], + "name": "sentry.javascript.node", + "packages": Array [ + Object { + "name": "npm:@sentry/node", + "version": "7.87.0", + }, + ], + "version": "7.87.0", + }, + "timestamp": 0, + }, + "header": Object { + "event_id": "", + "sdk": Object { + "name": "sentry.javascript.node", + "version": "7.87.0", + }, + "sent_at": "", + "trace": Object { + "environment": "production", + "public_key": "9edbb8417b284aa2bbead9b4c318918b", + "release": "", + "trace_id": "", + }, + }, + "type": Object { + "type": "event", + }, + } + `); }); }); }); function mockSentryEndpoint() { - const requests = { count: 0 }; + const requests: EnvelopeRequest[] = []; msw.use( rest.post( `https://platform.dash.cloudflare.com/sentry/envelope`, async (req, res, cxt) => { - requests.count++; + requests.push(await req.json()); return res(cxt.status(200), cxt.json({})); } ) diff --git a/packages/wrangler/src/api/mtls-certificate.ts b/packages/wrangler/src/api/mtls-certificate.ts index 0617b0a044fc..5021ca69d2aa 100644 --- a/packages/wrangler/src/api/mtls-certificate.ts +++ b/packages/wrangler/src/api/mtls-certificate.ts @@ -1,4 +1,5 @@ import { fetchResult } from "../cfetch"; +import { UserError } from "../errors"; import { readFileSync } from "../parse"; /** @@ -44,12 +45,12 @@ export interface MTlsCertificateListFilter { /** * indicates that looking up a certificate by name failed due to zero matching results */ -export class ErrorMTlsCertificateNameNotFound extends Error {} +export class ErrorMTlsCertificateNameNotFound extends UserError {} /** * indicates that looking up a certificate by name failed due to more than one matching results */ -export class ErrorMTlsCertificateManyNamesMatch extends Error {} +export class ErrorMTlsCertificateManyNamesMatch extends UserError {} /** * reads an mTLS certificate and private key pair from disk and uploads it to the account mTLS certificate store diff --git a/packages/wrangler/src/cfetch/index.ts b/packages/wrangler/src/cfetch/index.ts index 0b65ac7c25a2..e135a24e57ba 100644 --- a/packages/wrangler/src/cfetch/index.ts +++ b/packages/wrangler/src/cfetch/index.ts @@ -1,6 +1,6 @@ import { URLSearchParams } from "node:url"; import { logger } from "../logger"; -import { ParseError } from "../parse"; +import { APIError } from "../parse"; import { maybeThrowFriendlyError } from "./errors"; import { fetchInternal, performApiFetch } from "./internal"; import type { FetchError } from "./errors"; @@ -164,7 +164,7 @@ function throwFetchError( ): never { for (const error of response.errors) maybeThrowFriendlyError(error); - const error = new ParseError({ + const error = new APIError({ text: `A request to the Cloudflare API (${resource}) failed.`, notes: [ ...response.errors.map((err) => ({ text: renderError(err) })), diff --git a/packages/wrangler/src/cfetch/internal.ts b/packages/wrangler/src/cfetch/internal.ts index 0c3d6dda2690..081489f63106 100644 --- a/packages/wrangler/src/cfetch/internal.ts +++ b/packages/wrangler/src/cfetch/internal.ts @@ -3,8 +3,9 @@ import { fetch, File, Headers } from "undici"; import { Response } from "undici"; import { version as wranglerVersion } from "../../package.json"; import { getCloudflareApiBaseUrl } from "../environment-variables/misc-variables"; +import { UserError } from "../errors"; import { logger } from "../logger"; -import { ParseError, parseJSON } from "../parse"; +import { APIError, parseJSON } from "../parse"; import { loginOrRefreshIfRequired, requireApiToken } from "../user"; import type { ApiCredentials } from "../user"; import type { URLSearchParams } from "node:url"; @@ -94,7 +95,7 @@ export async function fetchInternal( try { return parseJSON(jsonText); } catch (err) { - throw new ParseError({ + throw new APIError({ text: "Received a malformed response from the API", notes: [ { @@ -129,7 +130,7 @@ function cloneHeaders( async function requireLoggedIn(): Promise { const loggedIn = await loginOrRefreshIfRequired(); if (!loggedIn) { - throw new Error("Not logged in."); + throw new UserError("Not logged in."); } } diff --git a/packages/wrangler/src/config/index.ts b/packages/wrangler/src/config/index.ts index c9a119bc652c..d6106912c19a 100644 --- a/packages/wrangler/src/config/index.ts +++ b/packages/wrangler/src/config/index.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import dotenv from "dotenv"; import { findUpSync } from "find-up"; +import { UserError } from "../errors"; import { logger } from "../logger"; import { parseJSONC, parseTOML, readFileSync } from "../parse"; import { normalizeAndValidateConfig } from "./validation"; @@ -52,7 +53,7 @@ export function readConfig( logger.warn(diagnostics.renderWarnings()); } if (diagnostics.hasErrors()) { - throw new Error(diagnostics.renderErrors()); + throw new UserError(diagnostics.renderErrors()); } return config; diff --git a/packages/wrangler/src/constellation/createProject.tsx b/packages/wrangler/src/constellation/createProject.tsx index e60588abfed7..490a759ca9a0 100644 --- a/packages/wrangler/src/constellation/createProject.tsx +++ b/packages/wrangler/src/constellation/createProject.tsx @@ -1,5 +1,6 @@ import { fetchResult } from "../cfetch"; import { withConfig } from "../config"; +import { UserError } from "../errors"; import { logger } from "../logger"; import { requireAuth } from "../user"; import { takeName } from "./options"; @@ -41,7 +42,7 @@ export const handler = withConfig( }); } catch (e) { if ((e as { code: number }).code === 7409) { - throw new Error("A project with that name already exists"); + throw new UserError("A project with that name already exists"); } throw e; } diff --git a/packages/wrangler/src/constellation/uploadModel.tsx b/packages/wrangler/src/constellation/uploadModel.tsx index c091336b0758..43357de3ae78 100644 --- a/packages/wrangler/src/constellation/uploadModel.tsx +++ b/packages/wrangler/src/constellation/uploadModel.tsx @@ -2,6 +2,7 @@ import { readFileSync } from "node:fs"; import { FormData, File } from "undici"; import { fetchResult } from "../cfetch"; import { withConfig } from "../config"; +import { UserError } from "../errors"; import { logger } from "../logger"; import { requireAuth } from "../user"; import { constellationBetaWarning, getProjectByName } from "./utils"; @@ -74,7 +75,7 @@ export const handler = withConfig( ); } catch (e) { if ((e as { code: number }).code === 7408) { - throw new Error("A model with that name already exists"); + throw new UserError("A model with that name already exists"); } throw e; } diff --git a/packages/wrangler/src/constellation/utils.ts b/packages/wrangler/src/constellation/utils.ts index 11728565c39c..96463eff337e 100644 --- a/packages/wrangler/src/constellation/utils.ts +++ b/packages/wrangler/src/constellation/utils.ts @@ -1,5 +1,6 @@ import { fetchResult } from "../cfetch"; import { getEnvironmentVariableFactory } from "../environment-variables/factory"; +import { UserError } from "../errors"; import type { Config } from "../config"; import type { Project, Model, CatalogEntry } from "./types"; @@ -20,7 +21,7 @@ export const getProjectByName = async ( const allProjects = await listProjects(accountId); const matchingProj = allProjects.find((proj) => proj.name === name); if (!matchingProj) { - throw new Error(`Couldn't find Project with name '${name}'`); + throw new UserError(`Couldn't find Project with name '${name}'`); } return matchingProj; }; @@ -34,7 +35,7 @@ export const getProjectModelByName = async ( const allModels = await listModels(accountId, proj); const matchingModel = allModels.find((model) => model.name === modelName); if (!matchingModel) { - throw new Error(`Couldn't find Model with name '${modelName}'`); + throw new UserError(`Couldn't find Model with name '${modelName}'`); } return matchingModel; }; diff --git a/packages/wrangler/src/d1/create.tsx b/packages/wrangler/src/d1/create.tsx index 5f5e00dc90e6..04b3d0128695 100644 --- a/packages/wrangler/src/d1/create.tsx +++ b/packages/wrangler/src/d1/create.tsx @@ -2,6 +2,7 @@ import { Box, Text } from "ink"; import React from "react"; import { fetchResult } from "../cfetch"; import { withConfig } from "../config"; +import { UserError } from "../errors"; import { logger } from "../logger"; import { requireAuth } from "../user"; import { renderToString } from "../utils/render"; @@ -35,7 +36,7 @@ export const Handler = withConfig( if (location) { if (LOCATION_CHOICES.indexOf(location.toLowerCase()) === -1) { - throw new Error( + throw new UserError( `Location '${location}' invalid. Valid values are ${LOCATION_CHOICES.join( "," )}` @@ -58,7 +59,7 @@ export const Handler = withConfig( }); } catch (e) { if ((e as { code: number }).code === 7502) { - throw new Error("A database with that name already exists"); + throw new UserError("A database with that name already exists"); } throw e; } diff --git a/packages/wrangler/src/d1/execute.tsx b/packages/wrangler/src/d1/execute.tsx index 66c6597e3498..135d4aa8878c 100644 --- a/packages/wrangler/src/d1/execute.tsx +++ b/packages/wrangler/src/d1/execute.tsx @@ -9,6 +9,7 @@ import { fetchResult } from "../cfetch"; import { readConfig } from "../config"; import { getLocalPersistencePath } from "../dev/get-local-persistence-path"; import { confirm } from "../dialogs"; +import { UserError } from "../errors"; import { logger } from "../logger"; import { readFileSync } from "../parse"; import { readableRelative } from "../paths"; @@ -179,18 +180,18 @@ export async function executeSql({ logger.loggerLevel = "error"; } const sql = file ? readFileSync(file) : command; - if (!sql) throw new Error(`Error: must provide --command or --file.`); + if (!sql) throw new UserError(`Error: must provide --command or --file.`); if (preview && local) - throw new Error(`Error: can't use --preview with --local`); + throw new UserError(`Error: can't use --preview with --local`); if (persistTo && !local) - throw new Error(`Error: can't use --persist-to without --local`); + throw new UserError(`Error: can't use --persist-to without --local`); logger.log(`🌀 Mapping SQL input into an array of statements`); const queries = splitSqlQuery(sql); if (file && sql) { if (queries[0].startsWith("SQLite format 3")) { //TODO: update this error to recommend using `wrangler d1 restore` when it exists - throw new Error( + throw new UserError( "Provided file is a binary SQLite database file instead of an SQL text file.\nThe execute command can only process SQL text files.\nPlease export an SQL file from your SQLite database and try again." ); } @@ -227,7 +228,7 @@ async function executeLocally({ }) { const localDB = getDatabaseInfoFromConfig(config, name); if (!localDB) { - throw new Error( + throw new UserError( `Can't find a DB with name/binding '${name}' in local config. Check info in wrangler.toml...` ); } @@ -312,9 +313,11 @@ async function executeRemotely({ name ); if (preview && !db.previewDatabaseUuid) { - throw logger.error( + const error = new UserError( "Please define a `preview_database_id` in your wrangler.toml to execute your queries against a preview database" ); + logger.error(error.message); + throw error; } const dbUuid = preview ? db.previewDatabaseUuid : db.uuid; logger.log(`🌀 Executing on remote database ${name} (${dbUuid}):`); diff --git a/packages/wrangler/src/d1/migrations/apply.tsx b/packages/wrangler/src/d1/migrations/apply.tsx index 75d8de417c0a..73977b5d112e 100644 --- a/packages/wrangler/src/d1/migrations/apply.tsx +++ b/packages/wrangler/src/d1/migrations/apply.tsx @@ -6,6 +6,7 @@ import Table from "ink-table"; import React from "react"; import { withConfig } from "../../config"; import { confirm } from "../../dialogs"; +import { UserError } from "../../errors"; import { CI } from "../../is-ci"; import isInteractive from "../../is-interactive"; import { logger } from "../../logger"; @@ -52,7 +53,7 @@ export const ApplyHandler = withConfig( }): Promise => { const databaseInfo = getDatabaseInfoFromConfig(config, database); if (!databaseInfo && !local) { - throw new Error( + throw new UserError( `Can't find a DB with name/binding '${database}' in local config. Check info in wrangler.toml...` ); } @@ -219,7 +220,7 @@ Your database may not be available to serve requests during the migration, conti ); if (errorNotes.length > 0) { - throw new Error( + throw new UserError( errorNotes .map((err) => { return err; diff --git a/packages/wrangler/src/d1/migrations/create.tsx b/packages/wrangler/src/d1/migrations/create.tsx index b27a75704d2c..5553e327fc11 100644 --- a/packages/wrangler/src/d1/migrations/create.tsx +++ b/packages/wrangler/src/d1/migrations/create.tsx @@ -3,6 +3,7 @@ import path from "path"; import { Box, Text } from "ink"; import React from "react"; import { withConfig } from "../../config"; +import { UserError } from "../../errors"; import { logger } from "../../logger"; import { renderToString } from "../../utils/render"; import { DEFAULT_MIGRATION_PATH } from "../constants"; @@ -28,7 +29,7 @@ export const CreateHandler = withConfig( async ({ config, database, message }): Promise => { const databaseInfo = getDatabaseInfoFromConfig(config, database); if (!databaseInfo) { - throw new Error( + throw new UserError( `Can't find a DB with name/binding '${database}' in local config. Check info in wrangler.toml...` ); } diff --git a/packages/wrangler/src/d1/migrations/helpers.ts b/packages/wrangler/src/d1/migrations/helpers.ts index 7f7bd366b145..702c08c96d32 100644 --- a/packages/wrangler/src/d1/migrations/helpers.ts +++ b/packages/wrangler/src/d1/migrations/helpers.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import path from "path"; import { confirm } from "../../dialogs"; +import { UserError } from "../../errors"; import { CI } from "../../is-ci"; import isInteractive from "../../is-interactive"; import { logger } from "../../logger"; @@ -35,7 +36,7 @@ export async function getMigrationsPath({ logger.warn(warning); } - throw new Error(`No migrations present at ${dir}.`); + throw new UserError(`No migrations present at ${dir}.`); } export async function getUnappliedMigrations({ diff --git a/packages/wrangler/src/d1/migrations/list.tsx b/packages/wrangler/src/d1/migrations/list.tsx index 6add5d68ac7b..bfa3de41dee5 100644 --- a/packages/wrangler/src/d1/migrations/list.tsx +++ b/packages/wrangler/src/d1/migrations/list.tsx @@ -3,6 +3,7 @@ import { Box, Text } from "ink"; import Table from "ink-table"; import React from "react"; import { withConfig } from "../../config"; +import { UserError } from "../../errors"; import { logger } from "../../logger"; import { requireAuth } from "../../user"; import { renderToString } from "../../utils/render"; @@ -33,7 +34,7 @@ export const ListHandler = withConfig( const databaseInfo = getDatabaseInfoFromConfig(config, database); if (!databaseInfo && !local) { - throw new Error( + throw new UserError( `Can't find a DB with name/binding '${database}' in local config. Check info in wrangler.toml...` ); } diff --git a/packages/wrangler/src/d1/timeTravel/restore.ts b/packages/wrangler/src/d1/timeTravel/restore.ts index e12b3fa64b72..f6320a978bcc 100644 --- a/packages/wrangler/src/d1/timeTravel/restore.ts +++ b/packages/wrangler/src/d1/timeTravel/restore.ts @@ -2,6 +2,7 @@ import { printWranglerBanner } from "../.."; import { fetchResult } from "../../cfetch"; import { withConfig } from "../../config"; import { confirm } from "../../dialogs"; +import { UserError } from "../../errors"; import { logger } from "../../logger"; import { requireAuth } from "../../user"; import { Database } from "../options"; @@ -39,11 +40,11 @@ export function RestoreOptions(yargs: CommonYargsArgv) { ) { return true; } else if (argv.timestamp && argv.bookmark) { - throw new Error( + throw new UserError( "Provide either a timestamp, or a bookmark - not both." ); } else { - throw new Error("Provide either a timestamp or a bookmark"); + throw new UserError("Provide either a timestamp or a bookmark"); } }); } diff --git a/packages/wrangler/src/d1/timeTravel/utils.ts b/packages/wrangler/src/d1/timeTravel/utils.ts index 2d4499667d12..b14691e5c3cb 100644 --- a/packages/wrangler/src/d1/timeTravel/utils.ts +++ b/packages/wrangler/src/d1/timeTravel/utils.ts @@ -1,4 +1,5 @@ import { fetchResult } from "../../cfetch"; +import { UserError } from "../../errors"; import { getDatabaseInfoFromId } from "../utils"; import type { BookmarkResponse } from "./types"; @@ -37,7 +38,7 @@ export const checkIfDatabaseIsExperimental = async ( ): Promise => { const dbInfo = await getDatabaseInfoFromId(accountId, databaseId); if (dbInfo.version !== "beta") { - throw new Error( + throw new UserError( "Time travel is only available for D1 databases created with the --experimental-backend flag" ); } @@ -77,7 +78,7 @@ export const convertTimestampToISO = (timestamp: string): string => { : new Date(Number(timestamp.length === 10 ? timestamp + "000" : timestamp)); if (parsedTimestamp.toString() === "Invalid Date") { - throw new Error( + throw new UserError( `Invalid timestamp '${timestamp}'. Please provide a valid Unix timestamp or ISO string, for example: ${getLocalISOString( new Date() )}\nFor accepted format, see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format` @@ -89,12 +90,12 @@ export const convertTimestampToISO = (timestamp: string): string => { const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); if (parsedTimestamp > now) { - throw new Error( + throw new UserError( `Invalid timestamp '${timestamp}'. Please provide a timestamp in the past` ); } if (parsedTimestamp < thirtyDaysAgo) { - throw new Error( + throw new UserError( `Invalid timestamp '${timestamp}'. Please provide a timestamp within the last 30 days` ); } diff --git a/packages/wrangler/src/d1/trimmer.ts b/packages/wrangler/src/d1/trimmer.ts index 48a43e2b0b1e..168e4260e09f 100644 --- a/packages/wrangler/src/d1/trimmer.ts +++ b/packages/wrangler/src/d1/trimmer.ts @@ -1,6 +1,8 @@ // Note that sqlite has many ways to trigger a transaction: https://www.sqlite.org/lang_transaction.html // this files (initial?) aim is to detect SQL files created by sqlite's .dump CLI command, and strip out the wrapping transaction in the sql file. +import { UserError } from "../errors"; + /** * A function to remove transaction statements from the start and end of SQL files, as the D1 API already does it for us. * @param sql a potentially large string of SQL statements @@ -15,7 +17,7 @@ export function trimSqlQuery(sql: string): string { .replace("COMMIT;", ""); //if the trimmed output STILL contains transactions, we should just tell them to remove them and try again. if (mayContainTransaction(trimmedSql)) { - throw new Error( + throw new UserError( "Wrangler could not process the provided SQL file, as it contains several transactions.\nD1 runs your SQL in a transaction for you.\nPlease export an SQL file from your SQLite database and try again." ); } diff --git a/packages/wrangler/src/d1/utils.ts b/packages/wrangler/src/d1/utils.ts index 61644702292a..93115bab3719 100644 --- a/packages/wrangler/src/d1/utils.ts +++ b/packages/wrangler/src/d1/utils.ts @@ -1,4 +1,5 @@ import { fetchResult } from "../cfetch"; +import { UserError } from "../errors"; import { DEFAULT_MIGRATION_PATH, DEFAULT_MIGRATION_TABLE } from "./constants"; import { listDatabases } from "./list"; import type { Config } from "../config"; @@ -40,7 +41,7 @@ export const getDatabaseByNameOrBinding = async ( const allDBs = await listDatabases(accountId); const matchingDB = allDBs.find((db) => db.name === name); if (!matchingDB) { - throw new Error(`Couldn't find DB with name '${name}'`); + throw new UserError(`Couldn't find DB with name '${name}'`); } return matchingDB; }; diff --git a/packages/wrangler/src/delete.ts b/packages/wrangler/src/delete.ts index c38a70f61aab..d8a51d8ffe89 100644 --- a/packages/wrangler/src/delete.ts +++ b/packages/wrangler/src/delete.ts @@ -3,6 +3,7 @@ import path from "path"; import { fetchResult } from "./cfetch"; import { findWranglerToml, readConfig } from "./config"; import { confirm } from "./dialogs"; +import { UserError } from "./errors"; import { deleteKVNamespace, listKVNamespaces } from "./kv/helpers"; import { logger } from "./logger"; import * as metrics from "./metrics"; @@ -105,11 +106,11 @@ export async function deleteHandler(args: DeleteArgs) { const accountId = args.dryRun ? undefined : await requireAuth(config); const scriptName = getScriptName(args, config); - - assert( - scriptName, - "A worker name must be defined, either via --name, or in wrangler.toml" - ); + if (!scriptName) { + throw new UserError( + "A worker name must be defined, either via --name, or in wrangler.toml" + ); + } if (args.dryRun) { logger.log(`--dry-run: exiting now.`); diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index ad05f0c81778..56fa14c6c041 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -23,9 +23,10 @@ import { import { addHyphens } from "../deployments"; import { confirm } from "../dialogs"; import { getMigrationsToUpload } from "../durable"; +import { UserError } from "../errors"; import { logger } from "../logger"; import { getMetricsUsageHeaders } from "../metrics"; -import { ParseError } from "../parse"; +import { APIError, ParseError } from "../parse"; import { getWranglerTmpDir } from "../paths"; import { getQueue, putConsumer } from "../queues/client"; import { getWorkersDevSubdomain } from "../routes"; @@ -322,7 +323,7 @@ export default async function deploy(props: Props): Promise { "" ).padStart(2, "0")}-${(new Date().getDate() + "").padStart(2, "0")}`; - throw new Error(`A compatibility_date is required when publishing. Add the following to your wrangler.toml file:. + throw new UserError(`A compatibility_date is required when publishing. Add the following to your wrangler.toml file:. \`\`\` compatibility_date = "${compatibilityDateStr}" \`\`\` @@ -338,12 +339,12 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m for (const route of routes) { if (typeof route !== "string" && route.custom_domain) { if (route.pattern.includes("*")) { - throw new Error( + throw new UserError( `Cannot use "${route.pattern}" as a Custom Domain; wildcard operators (*) are not allowed` ); } if (route.pattern.includes("/")) { - throw new Error( + throw new UserError( `Cannot use "${route.pattern}" as a Custom Domain; paths are not allowed` ); } @@ -434,25 +435,25 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m Boolean(props.assetPaths) && format === "service-worker" ) { - throw new Error( + throw new UserError( "You cannot use the service-worker format with an `assets` directory yet. For information on how to migrate to the module-worker format, see: https://developers.cloudflare.com/workers/learning/migrating-to-module-workers/" ); } if (config.wasm_modules && format === "modules") { - throw new Error( + throw new UserError( "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code" ); } if (config.text_blobs && format === "modules") { - throw new Error( + throw new UserError( "You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[rules]` in your wrangler.toml" ); } if (config.data_blobs && format === "modules") { - throw new Error( + throw new UserError( "You cannot configure [data_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[rules]` in your wrangler.toml" ); } @@ -711,11 +712,13 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m // Apply source mapping to validation startup errors if possible if ( - err instanceof ParseError && + err instanceof APIError && "code" in err && err.code === 10021 /* validation error */ && err.notes.length > 0 ) { + err.preventReport(); + const maybeNameToFilePath = (moduleName: string) => { // If this is a service worker, always return the entrypoint path. // Service workers can't have additional JavaScript modules. @@ -849,7 +852,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m ) )} to unassign a worker from a route.`; - throw new Error(`${errorMessage}\n${resolution}\n${dashLink}`); + throw new UserError(`${errorMessage}\n${resolution}\n${dashLink}`); } } } @@ -986,7 +989,7 @@ async function publishRoutesFallback( { scriptName, notProd }: { scriptName: string; notProd: boolean } ) { if (notProd) { - throw new Error( + throw new UserError( "Service environments combined with an API token that doesn't have 'All Zones' permissions is not supported.\n" + "Either turn off service environments by setting `legacy_env = true`, creating an API token with 'All Zones' permissions, or logging in via OAuth" ); @@ -1047,7 +1050,7 @@ async function publishRoutesFallback( alreadyDeployedRoutes.delete(routePattern); continue; } else { - throw new Error( + throw new UserError( `The route with pattern "${routePattern}" is already associated with another worker called "${knownScript}".` ); } @@ -1083,6 +1086,7 @@ async function publishRoutesFallback( } export function isAuthenticationError(e: unknown): e is ParseError { + // TODO: don't want to report these return e instanceof ParseError && (e as { code?: number }).code === 10000; } @@ -1102,7 +1106,7 @@ async function ensureQueuesExist(config: Config) { const queueErr = err as FetchError; if (queueErr.code === 11000) { // queue_not_found - throw new Error( + throw new UserError( `Queue "${queue}" does not exist. To create it, run: wrangler queues create ${queue}` ); } @@ -1128,7 +1132,7 @@ function updateQueueConsumers(config: Config): Promise[] { if (config.name === undefined) { // TODO: how can we reliably get the current script name? - throw new Error("Script name is required to update queue consumers"); + throw new UserError("Script name is required to update queue consumers"); } const scriptName = config.name; const envName = undefined; // TODO: script environment for wrangler deploy? diff --git a/packages/wrangler/src/deploy/index.ts b/packages/wrangler/src/deploy/index.ts index 144e1735bf7b..ae21849d7a45 100644 --- a/packages/wrangler/src/deploy/index.ts +++ b/packages/wrangler/src/deploy/index.ts @@ -3,6 +3,7 @@ import chalk from "chalk"; import { fetchResult } from "../cfetch"; import { findWranglerToml, readConfig } from "../config"; import { getEntry } from "../deployment-bundle/entry"; +import { UserError } from "../errors"; import { getRules, getScriptName, @@ -253,16 +254,18 @@ export async function deployHandler( ); if (args.public) { - throw new Error("The --public field has been renamed to --assets"); + throw new UserError("The --public field has been renamed to --assets"); } if (args.experimentalPublic) { - throw new Error( + throw new UserError( "The --experimental-public field has been renamed to --assets" ); } if ((args.assets || config.assets) && (args.site || config.site)) { - throw new Error("Cannot use Assets and Workers Sites in the same Worker."); + throw new UserError( + "Cannot use Assets and Workers Sites in the same Worker." + ); } if (args.assets) { diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts index 4e7e21020e4e..66c5b87065da 100644 --- a/packages/wrangler/src/deployment-bundle/bundle.ts +++ b/packages/wrangler/src/deployment-bundle/bundle.ts @@ -3,6 +3,7 @@ import * as path from "node:path"; import NodeGlobalsPolyfills from "@esbuild-plugins/node-globals-polyfill"; import NodeModulesPolyfills from "@esbuild-plugins/node-modules-polyfill"; import * as esbuild from "esbuild"; +import { UserError } from "../errors"; import { getBasePath, getWranglerTmpDir } from "../paths"; import { applyMiddlewareLoaderFacade } from "./apply-middleware"; import { @@ -248,7 +249,7 @@ export async function bundleWorker( // Check that the current worker format is supported by all the active middleware for (const middleware of middlewareToLoad) { if (!middleware.supports.includes(entry.format)) { - throw new Error( + throw new UserError( `Your Worker is written using the "${entry.format}" format, which isn't supported by the "${middleware.name}" middleware. To use "${middleware.name}" middleware, convert your Worker to the "${middleware.supports[0]}" format` ); } @@ -356,7 +357,7 @@ export async function bundleWorker( await ctx.watch(); result = await initialBuildResultPromise; if (result.errors.length > 0) { - throw new Error("Failed to build"); + throw new UserError("Failed to build"); } stop = async function () { @@ -383,7 +384,7 @@ export async function bundleWorker( .map((x) => x.class_name); if (notExportedDOs.length) { const relativePath = path.relative(process.cwd(), entryFile); - throw new Error( + throw new UserError( `Your Worker depends on the following Durable Objects, which are not exported in your entrypoint file: ${notExportedDOs.join( ", " )}.\nYou should export these objects from your entrypoint, ${relativePath}.` diff --git a/packages/wrangler/src/deployment-bundle/capnp.ts b/packages/wrangler/src/deployment-bundle/capnp.ts index c28beba30e13..e4ecec6a37de 100644 --- a/packages/wrangler/src/deployment-bundle/capnp.ts +++ b/packages/wrangler/src/deployment-bundle/capnp.ts @@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { sync as commandExistsSync } from "command-exists"; +import { UserError } from "../errors"; import type { CfCapnp } from "./worker"; export function handleUnsafeCapnp(capnp: CfCapnp): Buffer { @@ -14,7 +15,7 @@ export function handleUnsafeCapnp(capnp: CfCapnp): Buffer { resolve(base_path as string, x) ); if (!commandExistsSync("capnp")) { - throw new Error( + throw new UserError( "The capnp compiler is required to upload capnp schemas, but is not present." ); } diff --git a/packages/wrangler/src/deployment-bundle/entry.ts b/packages/wrangler/src/deployment-bundle/entry.ts index 30f6d2c65d94..5c7c3786629b 100644 --- a/packages/wrangler/src/deployment-bundle/entry.ts +++ b/packages/wrangler/src/deployment-bundle/entry.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { UserError } from "../errors"; import { logger } from "../logger"; import { getBasePath } from "../paths"; import guessWorkerFormat from "./guess-worker-format"; @@ -56,7 +57,7 @@ export async function getEntry( } else if (args.assets || config.assets) { file = path.resolve(getBasePath(), "templates/no-op-worker.js"); } else { - throw new Error( + throw new UserError( `Missing entry-point: The entry-point should be specified via the command line (e.g. \`wrangler ${command} path/to/script\`) or the \`main\` config field.` ); } @@ -97,7 +98,7 @@ export async function getEntry( "Alternatively, migrate your worker to ES Module syntax to implement a Durable Object in this Worker:"; const migrateUrl = "https://developers.cloudflare.com/workers/learning/migrating-to-module-workers/"; - throw new Error( + throw new UserError( `${errorMessage}\n${addScriptName}\n${addScriptNameExamples}\n${migrateText}\n${migrateUrl}` ); } diff --git a/packages/wrangler/src/deployment-bundle/find-additional-modules.ts b/packages/wrangler/src/deployment-bundle/find-additional-modules.ts index 40014df4b82d..5259bbff4cdd 100644 --- a/packages/wrangler/src/deployment-bundle/find-additional-modules.ts +++ b/packages/wrangler/src/deployment-bundle/find-additional-modules.ts @@ -2,6 +2,7 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import chalk from "chalk"; import globToRegExp from "glob-to-regexp"; +import { UserError } from "../errors"; import { logger } from "../logger"; import { RuleTypeToModuleType } from "./module-collection"; import { parseRules } from "./rules"; @@ -112,7 +113,7 @@ async function matchFiles( for (const glob of rule.globs) { const regexp = globToRegExp(glob); if (regexp.test(filePath)) { - throw new Error( + throw new UserError( `The file ${filePath} matched a module rule in your configuration (${JSON.stringify( rule )}), but was ignored because a previous rule with the same type was not marked as \`fallthrough = true\`.` diff --git a/packages/wrangler/src/deployment-bundle/guess-worker-format.ts b/packages/wrangler/src/deployment-bundle/guess-worker-format.ts index 84067beead14..3efc0df4ac42 100644 --- a/packages/wrangler/src/deployment-bundle/guess-worker-format.ts +++ b/packages/wrangler/src/deployment-bundle/guess-worker-format.ts @@ -1,5 +1,6 @@ import path from "node:path"; import * as esbuild from "esbuild"; +import { UserError } from "../errors"; import { logger } from "../logger"; import { COMMON_ESBUILD_OPTIONS } from "./bundle"; import { getEntryPointFromMetafile } from "./entry-point-from-metafile"; @@ -54,11 +55,11 @@ export default async function guessWorkerFormat( if (hint) { if (hint !== guessedWorkerFormat) { if (hint === "service-worker") { - throw new Error( + throw new UserError( "You configured this worker to be a 'service-worker', but the file you are trying to build appears to have a `default` export like a module worker. Please pass `--format modules`, or simply remove the configuration." ); } else { - throw new Error( + throw new UserError( "You configured this worker to be 'modules', but the file you are trying to build doesn't export a handler. Please pass `--format service-worker`, or simply remove the configuration." ); } diff --git a/packages/wrangler/src/deployment-bundle/module-collection.ts b/packages/wrangler/src/deployment-bundle/module-collection.ts index 3ce49ece5221..dc3bce1916ee 100644 --- a/packages/wrangler/src/deployment-bundle/module-collection.ts +++ b/packages/wrangler/src/deployment-bundle/module-collection.ts @@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import globToRegExp from "glob-to-regexp"; import { exports as resolveExports } from "resolve.exports"; +import { UserError } from "../errors"; import { logger } from "../logger"; import { BUILD_CONDITIONS } from "./bundle"; import { @@ -367,7 +368,7 @@ export function createModuleCollector(props: { build.onResolve( { filter: globToRegExp(glob) }, async (args: esbuild.OnResolveArgs) => { - throw new Error( + throw new UserError( `The file ${ args.path } matched a module rule in your configuration (${JSON.stringify( diff --git a/packages/wrangler/src/deployment-bundle/run-custom-build.ts b/packages/wrangler/src/deployment-bundle/run-custom-build.ts index a1bf5a5e6970..6babb93fb1d6 100644 --- a/packages/wrangler/src/deployment-bundle/run-custom-build.ts +++ b/packages/wrangler/src/deployment-bundle/run-custom-build.ts @@ -1,6 +1,7 @@ import { existsSync, statSync } from "node:fs"; import path from "node:path"; import { execaCommand } from "execa"; +import { UserError } from "../errors"; import { logger } from "../logger"; import type { Config } from "../config"; @@ -51,7 +52,7 @@ function assertEntryPointExists( errorMessage: string ) { if (!fileExists(expectedEntryAbsolute)) { - throw new Error( + throw new UserError( getMissingEntryPointMessage( errorMessage, expectedEntryAbsolute, diff --git a/packages/wrangler/src/deployments.ts b/packages/wrangler/src/deployments.ts index 1327e06977ae..001babe2538f 100644 --- a/packages/wrangler/src/deployments.ts +++ b/packages/wrangler/src/deployments.ts @@ -5,6 +5,7 @@ import { FormData } from "undici"; import { fetchResult } from "./cfetch"; import { readConfig } from "./config"; import { confirm, prompt } from "./dialogs"; +import { UserError } from "./errors"; import { mapBindings } from "./init"; import { logger } from "./logger"; import * as metrics from "./metrics"; @@ -155,14 +156,14 @@ export async function rollbackDeployment( ); if (deploys.length < 2) { - throw new Error( + throw new UserError( "Cannot rollback to previous deployment since there are less than 2 deployments" ); } deploymentId = deploys.at(-2)?.id; if (deploymentId === undefined) { - throw new Error("Cannot find previous deployment"); + throw new UserError("Cannot find previous deployment"); } } @@ -264,7 +265,7 @@ export async function viewDeployment( deploymentId = latest.id; if (deploymentId === undefined) { - throw new Error("Cannot find previous deployment"); + throw new UserError("Cannot find previous deployment"); } } @@ -336,7 +337,7 @@ export async function commonDeploymentCMDSetup( logger.log(`${deploymentsWarning}\n`); if (!scriptName) { - throw new Error( + throw new UserError( "Required Worker name missing. Please specify the Worker name in wrangler.toml, or pass it as an argument with `--name`" ); } diff --git a/packages/wrangler/src/dev.tsx b/packages/wrangler/src/dev.tsx index 08ac8402fc55..ceb2993bcee8 100644 --- a/packages/wrangler/src/dev.tsx +++ b/packages/wrangler/src/dev.tsx @@ -11,6 +11,7 @@ import { getVarsForDev } from "./dev/dev-vars"; import { getLocalPersistencePath } from "./dev/get-local-persistence-path"; import { startDevServer } from "./dev/start-server"; +import { UserError } from "./errors"; import { logger } from "./logger"; import * as metrics from "./metrics"; import { getAssetPaths, getSiteAssetPaths } from "./sites"; @@ -239,7 +240,7 @@ export function devOptions(yargs: CommonYargsArgv) { }) .check((argv) => { if (argv["live-reload"] && argv.remote) { - throw new Error( + throw new UserError( "--live-reload is only supported in local mode. Please just use one of either --remote or --live-reload." ); } @@ -285,7 +286,7 @@ This is currently not supported 😭, but we think that we'll get it to work soo if (args.remote) { const isLoggedIn = await loginOrRefreshIfRequired(); if (!isLoggedIn) { - throw new Error( + throw new UserError( "You must be logged in to use wrangler dev in remote mode. Try logging in, or run wrangler dev --local." ); } @@ -614,7 +615,9 @@ export async function startApiDev(args: StartDevOptions) { const devServer = await getDevServer(config); if (!devServer) { - throw logger.error("Failed to start dev server."); + const error = new Error("Failed to start dev server."); + logger.error(error.message); + throw error; } return { @@ -675,7 +678,7 @@ async function getZoneIdHostAndRoutes(args: StartDevOptions, config: Config) { // TODO(consider): do we need really need to do this? I've added the condition to throw to match the previous implicit behaviour of `new URL()` throwing upon invalid URLs, but could we just continue here without an inferred host? if (host === undefined) { - throw new Error( + throw new UserError( `Cannot infer host from first route: ${JSON.stringify( firstRoute )}.\nYou can explicitly set the \`dev.host\` configuration in your wrangler.toml file, for example: @@ -732,17 +735,19 @@ async function validateDevServerSettings( ); } if (args.experimentalPublic) { - throw new Error( + throw new UserError( "The --experimental-public field has been renamed to --assets" ); } if (args.public) { - throw new Error("The --public field has been renamed to --assets"); + throw new UserError("The --public field has been renamed to --assets"); } if ((args.assets ?? config.assets) && (args.site ?? config.site)) { - throw new Error("Cannot use Assets and Workers Sites in the same Worker."); + throw new UserError( + "Cannot use Assets and Workers Sites in the same Worker." + ); } if (args.assets) { @@ -770,7 +775,7 @@ async function validateDevServerSettings( args.compatibilityFlags ?? config.compatibility_flags; const nodejsCompat = compatibilityFlags?.includes("nodejs_compat"); if (legacyNodeCompat && nodejsCompat) { - throw new Error( + throw new UserError( "The `nodejs_compat` compatibility flag cannot be used in conjunction with the legacy `--node-compat` flag. If you want to use the Workers runtime Node.js compatibility features, please remove the `--node-compat` argument from your CLI command or `node_compat = true` from your config file." ); } @@ -860,7 +865,7 @@ function getBindings( if (!preview_id && !local) { // TODO: This error has to be a _lot_ better, ideally just asking // to create a preview namespace for the user automatically - throw new Error( + throw new UserError( `In development, you should use a separate kv namespace than the one you'd use in production. Please create a new kv namespace with "wrangler kv:namespace create --preview" and add its id as preview_id to the kv_namespace "${binding}" in your wrangler.toml` ); // Ugh, I really don't like this message very much } @@ -900,7 +905,7 @@ function getBindings( // same idea as kv namespace preview id, // same copy-on-write TODO if (!preview_bucket_name && !local) { - throw new Error( + throw new UserError( `In development, you should use a separate r2 bucket than the one you'd use in production. Please create a new r2 bucket with "wrangler r2 bucket create " and add its name as preview_bucket_name to the r2_buckets "${binding}" in your wrangler.toml` ); } @@ -946,7 +951,7 @@ function getBindings( constellation: configParam.constellation, hyperdrive: configParam.hyperdrive.map((hyperdrive) => { if (!hyperdrive.localConnectionString) { - throw new Error( + throw new UserError( `In development, you should use a local postgres connection string to emulate hyperdrive functionality. Please setup postgres locally and set the value of "${hyperdrive.binding}"'s "localConnectionString" to the postgres connection string in your wrangler.toml` ); } diff --git a/packages/wrangler/src/dev/remote.tsx b/packages/wrangler/src/dev/remote.tsx index 709ded5f0209..84d42a971491 100644 --- a/packages/wrangler/src/dev/remote.tsx +++ b/packages/wrangler/src/dev/remote.tsx @@ -7,6 +7,7 @@ import { helpIfErrorIsSizeOrScriptStartup } from "../deploy/deploy"; import { printBundleSize } from "../deployment-bundle/bundle-reporter"; import { getBundleType } from "../deployment-bundle/bundle-type"; import { withSourceURLs } from "../deployment-bundle/source-url"; +import { UserError } from "../errors"; import { logger } from "../logger"; import { syncAssets } from "../sites"; import { @@ -401,9 +402,11 @@ export async function startRemoteServer(props: RemoteProps) { }); accountId = accountChoices[0].id; } else { - throw logger.error( + const error = new UserError( "In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as `account_id` in your `wrangler.toml` file." ); + logger.error(error.message); + throw error; } } @@ -413,7 +416,9 @@ export async function startRemoteServer(props: RemoteProps) { }); if (previewToken === undefined) { - throw logger.error("Failed to get a previewToken"); + const error = new Error("Failed to get a previewToken"); + logger.error(error.message); + throw error; } // start our proxy server const previewServer = await startPreviewServer({ @@ -452,7 +457,9 @@ export async function startRemoteServer(props: RemoteProps) { }, }); if (!previewServer) { - throw logger.error("Failed to start remote server"); + const error = new Error("Failed to start remote server"); + logger.error(error); + throw error; } return { stop: previewServer.stop }; } @@ -465,7 +472,9 @@ export async function getRemotePreviewToken(props: RemoteProps) { //setup the preview session async function start() { if (props.accountId === undefined) { - throw logger.error("no accountId provided"); + const error = new Error("no accountId provided"); + logger.error(error.message); + throw error; } const abortController = new AbortController(); const { workerAccount, workerContext } = getWorkerAccountAndContext({ @@ -485,7 +494,9 @@ export async function getRemotePreviewToken(props: RemoteProps) { //use the session to upload the worker, and create a preview if (session === undefined) { - throw logger.error("Failed to start a session"); + const error = new Error("Failed to start a session"); + logger.error(error.message); + throw error; } if (!props.bundle || !props.format) return; diff --git a/packages/wrangler/src/dev/validate-dev-props.ts b/packages/wrangler/src/dev/validate-dev-props.ts index 89eddca849d0..318b963ef20e 100644 --- a/packages/wrangler/src/dev/validate-dev-props.ts +++ b/packages/wrangler/src/dev/validate-dev-props.ts @@ -1,3 +1,4 @@ +import { UserError } from "../errors"; import type { DevProps } from "./dev"; export function validateDevProps(props: DevProps) { @@ -6,25 +7,25 @@ export function validateDevProps(props: DevProps) { props.assetPaths && props.entry.format === "service-worker" ) { - throw new Error( + throw new UserError( "You cannot use the service-worker format with an `assets` directory yet. For information on how to migrate to the module-worker format, see: https://developers.cloudflare.com/workers/learning/migrating-to-module-workers/" ); } if (props.bindings.wasm_modules && props.entry.format === "modules") { - throw new Error( + throw new UserError( "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code" ); } if (props.bindings.text_blobs && props.entry.format === "modules") { - throw new Error( + throw new UserError( "You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[rules]` in your wrangler.toml" ); } if (props.bindings.data_blobs && props.entry.format === "modules") { - throw new Error( + throw new UserError( "You cannot configure [data_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[rules]` in your wrangler.toml" ); } @@ -33,7 +34,7 @@ export function validateDevProps(props: DevProps) { props.compatibilityFlags?.includes("nodejs_compat") && props.legacyNodeCompat ) { - throw new Error( + throw new UserError( "You cannot use the `nodejs_compat` compatibility flag in conjunction with the legacy `--node-compat` flag. If you want to use the new runtime Node.js compatibility features, please remove the `--node-compat` argument from your CLI command or your config file." ); } diff --git a/packages/wrangler/src/dialogs.ts b/packages/wrangler/src/dialogs.ts index a735005cdb23..ca5aaccf4ea4 100644 --- a/packages/wrangler/src/dialogs.ts +++ b/packages/wrangler/src/dialogs.ts @@ -1,5 +1,6 @@ import chalk from "chalk"; import prompts from "prompts"; +import { UserError } from "./errors"; import { CI } from "./is-ci"; import isInteractive from "./is-interactive"; import { logger } from "./logger"; @@ -9,7 +10,7 @@ function isNonInteractiveOrCI(): boolean { return !isInteractive() || CI.isCI(); } -export class NoDefaultValueProvided extends Error { +export class NoDefaultValueProvided extends UserError { constructor() { // This is user-facing, so make the message something understandable // It _should_ always be caught and replaced with a more descriptive error diff --git a/packages/wrangler/src/errors.ts b/packages/wrangler/src/errors.ts index ce1a1f59378b..85892bb3b7cc 100644 --- a/packages/wrangler/src/errors.ts +++ b/packages/wrangler/src/errors.ts @@ -1,10 +1,25 @@ -export class DeprecationError extends Error { +/** + * Base class for errors where the user has done something wrong. These are not + * reported to Sentry. API errors are intentionally *not* `UserError`s, and are + * reported to Sentry. This will help us understand which API errors need better + * messaging. + */ +export class UserError extends Error { + constructor(...args: ConstructorParameters) { + super(...args); + // Restore prototype chain: + // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class DeprecationError extends UserError { constructor(message: string) { super(`Deprecation:\n${message}`); } } -export class FatalError extends Error { +export class FatalError extends UserError { constructor(message?: string, readonly code?: number) { super(message); } diff --git a/packages/wrangler/src/generate/index.ts b/packages/wrangler/src/generate/index.ts index 96e98b8f831f..0911fa79706d 100644 --- a/packages/wrangler/src/generate/index.ts +++ b/packages/wrangler/src/generate/index.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import path from "node:path"; +import { UserError } from "../errors"; import { cloneIntoDirectory, initializeGit } from "../git-client"; import { CommandLineArgsError, printWranglerBanner } from "../index"; import { initHandler } from "../init"; @@ -252,7 +253,7 @@ function toUrlBase({ httpsUrl, gitUrl, shorthandUrl }: TemplateRegexUrlGroup) { case "bb": return "https://bitbucket.org"; default: - throw new Error( + throw new UserError( `Unable to parse shorthand ${shorthandUrl}. Supported options are "bitbucket" ("bb"), "github" ("gh"), and "gitlab" ("gl")` ); } @@ -285,7 +286,7 @@ function parseTemplatePath(templatePath: string): { | undefined; if (!groups) { - throw new Error(`Unable to parse ${templatePath} as a template`); + throw new UserError(`Unable to parse ${templatePath} as a template`); } const { user, repository, subdirectoryPath, tag, ...urlGroups } = groups; diff --git a/packages/wrangler/src/git-client.ts b/packages/wrangler/src/git-client.ts index 01f2cb4e19d0..539b6604ac90 100644 --- a/packages/wrangler/src/git-client.ts +++ b/packages/wrangler/src/git-client.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { execa } from "execa"; import { findUp } from "find-up"; import semiver from "semiver"; +import { UserError } from "./errors"; import { logger } from "./logger"; /** * Check whether the given current working directory is within a git repository @@ -82,7 +83,7 @@ export async function cloneIntoDirectory( const gitVersion = await getGitVersioon(); if (!gitVersion) { - throw new Error("Failed to find git installation"); + throw new UserError("Failed to find git installation"); } // sparse checkouts were added in git 2.26.0, and allow for...sparse...checkouts... @@ -129,7 +130,7 @@ export async function cloneIntoDirectory( // @ts-expect-error non standard property on Error if (err.code !== "EXDEV") { logger.debug(err); - throw new Error(`Failed to find "${subdirectory}" in ${remote}`); + throw new UserError(`Failed to find "${subdirectory}" in ${remote}`); } // likely on a different filesystem, so we need to copy instead of rename // and then remove the original directory @@ -141,7 +142,7 @@ export async function cloneIntoDirectory( }); } catch (moveErr) { logger.debug(moveErr); - throw new Error(`Failed to find "${subdirectory}" in ${remote}`); + throw new UserError(`Failed to find "${subdirectory}" in ${remote}`); } } fs.rmSync(path.join(targetDirectory, ".git"), { diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index 6281b67845ac..3ca8e9e37e76 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -34,6 +34,7 @@ import { import { devHandler, devOptions } from "./dev"; import { workerNamespaceCommands } from "./dispatch-namespace"; import { docsHandler, docsOptions } from "./docs"; +import { UserError } from "./errors"; import { generateHandler, generateOptions } from "./generate"; import { hyperdrive } from "./hyperdrive/index"; import { initHandler, initOptions } from "./init"; @@ -43,7 +44,7 @@ import * as metrics from "./metrics"; import { mTlsCertificateCommands } from "./mtls-certificate/cli"; import { pages } from "./pages"; -import { formatMessage, ParseError } from "./parse"; +import { APIError, formatMessage, ParseError } from "./parse"; import { pubSubCommands } from "./pubsub/pubsub-commands"; import { queues } from "./queues/cli/commands"; import { r2 } from "./r2"; @@ -94,7 +95,7 @@ export function getRules(config: Config): Config["rules"] { const rules = config.rules ?? config.build?.upload?.rules ?? []; if (config.rules && config.build?.upload?.rules) { - throw new Error( + throw new UserError( `You cannot configure both [rules] and [build.upload.rules] in your wrangler.toml. Delete the \`build.upload\` section.` ); } @@ -169,7 +170,7 @@ export function demandOneOfOption(...options: string[]) { }; } -export class CommandLineArgsError extends Error {} +export class CommandLineArgsError extends UserError {} export function createCLIParser(argv: string[]) { // Type check result against CommonYargsOptions to make sure we've included @@ -716,14 +717,27 @@ export function createCLIParser(argv: string[]) { export async function main(argv: string[]): Promise { setupSentry(); - addBreadcrumb(`wrangler ${argv.join(" ")}`); const wrangler = createCLIParser(argv); + + // Register Yargs middleware to record command as Sentry breadcrumb + let recordedCommand = false; + const wranglerWithMiddleware = wrangler.middleware((args) => { + // Middleware called for each sub-command, but only want to record once + if (recordedCommand) return; + recordedCommand = true; + // `args._` doesn't include any positional arguments (e.g. script name, + // key to fetch) or flags + addBreadcrumb(`wrangler ${args._.join(" ")}`); + }, /* applyBeforeValidation */ true); + let cliHandlerThrew = false; try { - await wrangler.parse(); + await wranglerWithMiddleware.parse(); } catch (e) { cliHandlerThrew = true; + let mayReport = true; + logger.log(""); // Just adds a bit of space if (e instanceof CommandLineArgsError) { logger.error(e.message); @@ -732,6 +746,7 @@ export async function main(argv: string[]): Promise { // The `wrangler` object is "frozen"; we cannot reuse that with different args, so we must create a new CLI parser to generate the help message. await createCLIParser([...argv, "--help"]).parse(); } else if (isAuthenticationError(e)) { + mayReport = false; logger.log(formatMessage(e)); const envAuth = getAuthFromEnv(); if (envAuth !== undefined && "apiToken" in envAuth) { @@ -753,6 +768,7 @@ export async function main(argv: string[]): Promise { // the current terminal doesn't support raw mode, which Ink needs to render // Ink doesn't throw a typed error or subclass or anything, so we just check the message content. // https://github.com/vadimdemedes/ink/blob/546fe16541fd05ad4e638d6842ca4cbe88b4092b/src/components/App.tsx#L138-L148 + mayReport = false; const currentPlatform = os.platform(); @@ -773,6 +789,7 @@ export async function main(argv: string[]): Promise { `${thisTerminalIsUnsupported}\n${soWranglerWontWork}\n${tryRunningItIn}${oneOfThese}` ); } else if (isBuildFailure(e)) { + mayReport = false; logBuildFailure(e.errors, e.warnings); logger.error(e.message); } else { @@ -781,8 +798,19 @@ export async function main(argv: string[]): Promise { `${fgGreenColor}%s${resetColor}`, "If you think this is a bug then please create an issue at https://github.com/cloudflare/workers-sdk/issues/new/choose" ); + } + + if ( + // Only report the error if we didn't just handle it + mayReport && + // ...and it's not a user error + !(e instanceof UserError) && + // ...and it's not an un-reportable API error + !(e instanceof APIError && !e.reportable) + ) { await captureGlobalException(e); } + throw e; } finally { try { diff --git a/packages/wrangler/src/init.ts b/packages/wrangler/src/init.ts index f5bae36d197f..a83ee7236b45 100644 --- a/packages/wrangler/src/init.ts +++ b/packages/wrangler/src/init.ts @@ -11,6 +11,7 @@ import { fetchWorker } from "./cfetch/internal"; import { readConfig } from "./config"; import { confirm, select } from "./dialogs"; import { getC3CommandFromEnv } from "./environment-variables/misc-variables"; +import { UserError } from "./errors"; import { initializeGit, getGitVersioon, isInsideGitRepo } from "./git-client"; import { logger } from "./logger"; import { getPackageManager } from "./package-manager"; @@ -233,7 +234,7 @@ export async function initHandler(args: InitArgs) { ); } catch (err) { if ((err as { code?: number }).code === 10090) { - throw new Error( + throw new UserError( "wrangler couldn't find a Worker script with that name in your account.\nRun `wrangler whoami` to confirm you're logged into the correct account." ); } diff --git a/packages/wrangler/src/kv/helpers.ts b/packages/wrangler/src/kv/helpers.ts index f1b1df04b49c..3acc0b79c17a 100644 --- a/packages/wrangler/src/kv/helpers.ts +++ b/packages/wrangler/src/kv/helpers.ts @@ -4,6 +4,7 @@ import { FormData } from "undici"; import { fetchListResult, fetchResult, fetchKVGetValue } from "../cfetch"; import { getLocalPersistencePath } from "../dev/get-local-persistence-path"; import { buildPersistOptions } from "../dev/miniflare"; +import { UserError } from "../errors"; import { logger } from "../logger"; import type { Config } from "../config"; import type { KVNamespace } from "@cloudflare/workers-types/experimental"; @@ -347,12 +348,12 @@ export function getKVNamespaceId( // `--binding` is only valid if there's a wrangler configuration. if (binding && !config) { - throw new Error("--binding specified, but no config file was found."); + throw new UserError("--binding specified, but no config file was found."); } // there's no config. abort here if (!config) { - throw new Error( + throw new UserError( "Failed to find a config file.\n" + "Either use --namespace-id to upload directly or create a configuration file with a binding." ); @@ -360,7 +361,7 @@ export function getKVNamespaceId( // there's no KV namespaces if (!config.kv_namespaces || config.kv_namespaces.length === 0) { - throw new Error( + throw new UserError( "No KV Namespaces configured! Either use --namespace-id to upload directly or add a KV namespace to your wrangler config file." ); } @@ -369,7 +370,7 @@ export function getKVNamespaceId( // we couldn't find a namespace with that binding if (!namespace) { - throw new Error( + throw new UserError( `A namespace with binding name "${binding}" was not found in the configured "kv_namespaces".` ); } @@ -382,7 +383,7 @@ export function getKVNamespaceId( // We don't want to execute code below if preview is set to true, so we just return. Otherwise we will get errors! return namespaceId; } else if (preview) { - throw new Error( + throw new UserError( `No preview ID found for ${binding}. Add one to your wrangler config file to use a separate namespace for previewing your worker.` ); } @@ -397,7 +398,7 @@ export function getKVNamespaceId( // We don't want to execute code below if preview is set to true, so we just return. Otherwise we can get error! return namespaceId; } else if (previewIsDefined) { - throw new Error( + throw new UserError( `No namespace ID found for ${binding}. Add one to your wrangler config file to use a separate namespace for previewing your worker.` ); } @@ -409,7 +410,7 @@ export function getKVNamespaceId( if (bindingHasOnlyOneId) { namespaceId = namespace.id || namespace.preview_id; } else { - throw new Error( + throw new UserError( `${binding} has both a namespace ID and a preview ID. Specify "--preview" or "--preview false" to avoid writing data to the wrong namespace.` ); } @@ -417,7 +418,7 @@ export function getKVNamespaceId( // shouldn't happen. we should be able to prove this with strong typing. // TODO: when we add strongly typed commands, rewrite these checks so they're exhaustive if (!namespaceId) { - throw Error( + throw new Error( "Something went wrong trying to determine which namespace to upload to.\n" + "Please create a github issue with the command you just ran along with your wrangler configuration." ); diff --git a/packages/wrangler/src/kv/index.ts b/packages/wrangler/src/kv/index.ts index f55587c040fa..4c851ed7dca0 100644 --- a/packages/wrangler/src/kv/index.ts +++ b/packages/wrangler/src/kv/index.ts @@ -3,6 +3,7 @@ import { arrayBuffer } from "node:stream/consumers"; import { StringDecoder } from "node:string_decoder"; import { readConfig } from "../config"; import { confirm } from "../dialogs"; +import { UserError } from "../errors"; import { demandOneOfOption, printWranglerBanner, @@ -564,7 +565,7 @@ export const kvBulk = (kvYargs: CommonYargsArgv) => { const content = parseJSON(readFileSync(filename), filename); if (!Array.isArray(content)) { - throw new Error( + throw new UserError( `Unexpected JSON input from "${filename}".\n` + `Expected an array of key-value objects but got type "${typeof content}".` ); @@ -596,7 +597,7 @@ export const kvBulk = (kvYargs: CommonYargsArgv) => { ); } if (errors.length > 0) { - throw new Error( + throw new UserError( `Unexpected JSON input from "${filename}".\n` + `Each item in the array should be an object that matches:\n\n` + `interface KeyValue {\n` + @@ -699,7 +700,7 @@ export const kvBulk = (kvYargs: CommonYargsArgv) => { const content = parseJSON(readFileSync(filename), filename) as string[]; if (!Array.isArray(content)) { - throw new Error( + throw new UserError( `Unexpected JSON input from "${filename}".\n` + `Expected an array of strings but got:\n${content}` ); @@ -718,7 +719,7 @@ export const kvBulk = (kvYargs: CommonYargsArgv) => { } if (errors.length > 0) { - throw new Error( + throw new UserError( `Unexpected JSON input from "${filename}".\n` + `Expected an array of strings.\n` + errors.join("\n") diff --git a/packages/wrangler/src/package-manager.ts b/packages/wrangler/src/package-manager.ts index 20391c13af0d..b7fb6e9951f3 100644 --- a/packages/wrangler/src/package-manager.ts +++ b/packages/wrangler/src/package-manager.ts @@ -2,6 +2,7 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import { env } from "node:process"; import { execa, execaCommandSync } from "execa"; +import { UserError } from "./errors"; import { logger } from "./logger"; export interface PackageManager { @@ -86,7 +87,7 @@ export async function getPackageManager(cwd: string): Promise { logger.log("Using pnpm as package manager."); return { ...PnpmPackageManager, cwd }; } else { - throw new Error( + throw new UserError( "Unable to find a package manager. Supported managers are: npm, yarn, and pnpm." ); } diff --git a/packages/wrangler/src/pages/build.ts b/packages/wrangler/src/pages/build.ts index e50f288c5053..772573d46c4f 100644 --- a/packages/wrangler/src/pages/build.ts +++ b/packages/wrangler/src/pages/build.ts @@ -2,7 +2,7 @@ import { existsSync, lstatSync, mkdirSync, writeFileSync } from "node:fs"; import { basename, dirname, relative, resolve as resolvePath } from "node:path"; import { createUploadWorkerBundleContents } from "../api/pages/create-worker-bundle-contents"; import { writeAdditionalModules } from "../deployment-bundle/find-additional-modules"; -import { FatalError } from "../errors"; +import { FatalError, UserError } from "../errors"; import { logger } from "../logger"; import * as metrics from "../metrics"; import { buildFunctions } from "./buildFunctions"; @@ -358,7 +358,7 @@ const validateArgs = (args: PagesBuildArgs): ValidatedArgs => { } const nodejsCompat = !!args.compatibilityFlags?.includes("nodejs_compat"); if (legacyNodeCompat && nodejsCompat) { - throw new Error( + throw new UserError( "The `nodejs_compat` compatibility flag cannot be used in conjunction with the legacy `--node-compat` flag. If you want to use the Workers runtime Node.js compatibility features, please remove the `--node-compat` argument from your CLI command." ); } diff --git a/packages/wrangler/src/pages/deployment-tails.ts b/packages/wrangler/src/pages/deployment-tails.ts index bb124e68f120..7e625a840ff6 100644 --- a/packages/wrangler/src/pages/deployment-tails.ts +++ b/packages/wrangler/src/pages/deployment-tails.ts @@ -205,7 +205,7 @@ export async function Handler({ } if (!deploymentId || !projectName) { - throw new FatalError("An unknown error occurred.", 1); + throw new Error("An unknown error occurred."); } const filters = translateCLICommandToFilterMessage({ diff --git a/packages/wrangler/src/pages/dev.ts b/packages/wrangler/src/pages/dev.ts index d06e052c6c88..d93361437bb6 100644 --- a/packages/wrangler/src/pages/dev.ts +++ b/packages/wrangler/src/pages/dev.ts @@ -471,9 +471,8 @@ export const Handler = async ({ if (scriptPath === "") { // Failed to get a script with or without Functions, // something really bad must have happened. - throw new FatalError( - "Failed to start wrangler pages dev due to an unknown error", - 1 + throw new Error( + "Failed to start wrangler pages dev due to an unknown error" ); } diff --git a/packages/wrangler/src/pages/errors.ts b/packages/wrangler/src/pages/errors.ts index f966ff0708db..014bc1312afb 100644 --- a/packages/wrangler/src/pages/errors.ts +++ b/packages/wrangler/src/pages/errors.ts @@ -1,3 +1,4 @@ +import { UserError } from "../errors"; import { MAX_FUNCTIONS_ROUTES_RULES, MAX_FUNCTIONS_ROUTES_RULE_LENGTH, @@ -23,7 +24,7 @@ export const EXIT_CODE_FUNCTIONS_NOTHING_TO_BUILD_ERROR = 157; /** * Pages error when building a script from the functions directory fails */ -export class FunctionsBuildError extends Error { +export class FunctionsBuildError extends UserError { constructor(message: string) { super(message); } @@ -44,7 +45,7 @@ export function getFunctionsBuildWarning( /** * Pages error when no routes are found in the functions directory */ -export class FunctionsNoRoutesError extends Error { +export class FunctionsNoRoutesError extends UserError { constructor(message: string) { super(message); } diff --git a/packages/wrangler/src/pages/functions/routes.ts b/packages/wrangler/src/pages/functions/routes.ts index 7e640926e3ee..771e9340ea11 100755 --- a/packages/wrangler/src/pages/functions/routes.ts +++ b/packages/wrangler/src/pages/functions/routes.ts @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { UserError } from "../../errors"; import { isValidIdentifier, normalizeIdentifier } from "./identifiers"; import type { UrlPath } from "../../paths"; @@ -85,12 +86,12 @@ export function parseConfig(config: Config, baseDir: string) { // ensure the filepath isn't attempting to resolve to anything outside of the project if (path.relative(baseDir, resolvedPath).startsWith("..")) { - throw new Error(`Invalid module path "${filepath}"`); + throw new UserError(`Invalid module path "${filepath}"`); } // ensure the module name (if provided) is a valid identifier to guard against injection attacks if (name !== "default" && !isValidIdentifier(name)) { - throw new Error(`Invalid module identifier "${name}"`); + throw new UserError(`Invalid module identifier "${name}"`); } if (!identifier) { diff --git a/packages/wrangler/src/parse.ts b/packages/wrangler/src/parse.ts index 751dcd622ea8..1a7aed4a4cea 100644 --- a/packages/wrangler/src/parse.ts +++ b/packages/wrangler/src/parse.ts @@ -7,7 +7,9 @@ import { printParseErrorCode, type ParseError as JsoncParseError, } from "jsonc-parser"; +import { UserError } from "./errors"; import { logger } from "./logger"; + export type Message = { text: string; location?: Location; @@ -51,7 +53,7 @@ export function formatMessage( /** * An error that's thrown when something fails to parse. */ -export class ParseError extends Error implements Message { +export class ParseError extends UserError implements Message { readonly text: string; readonly notes: Message[]; readonly location?: Location; @@ -67,6 +69,21 @@ export class ParseError extends Error implements Message { } } +// `ParseError`s shouldn't generally be reported to Sentry, but Wrangler has +// relied on `ParseError` for any sort of error with additional notes. +// In particular, API errors which we'd like to report are `ParseError`s. +// Therefore, allow particular `ParseError`s to be marked `reportable`. +export class APIError extends ParseError { + // Allow `APIError`s to be marked as handled. + #reportable = true; + get reportable() { + return this.#reportable; + } + preventReport() { + this.#reportable = false; + } +} + const TOML_ERROR_NAME = "TomlError"; const TOML_ERROR_SUFFIX = " at row "; diff --git a/packages/wrangler/src/r2/helpers.ts b/packages/wrangler/src/r2/helpers.ts index 6afe48277c73..655cc051a4f3 100644 --- a/packages/wrangler/src/r2/helpers.ts +++ b/packages/wrangler/src/r2/helpers.ts @@ -3,6 +3,7 @@ import { fetchResult } from "../cfetch"; import { fetchR2Objects } from "../cfetch/internal"; import { getLocalPersistencePath } from "../dev/get-local-persistence-path"; import { buildPersistOptions } from "../dev/miniflare"; +import { UserError } from "../errors"; import type { R2Bucket } from "@cloudflare/workers-types/experimental"; import type { ReplaceWorkersTypes } from "miniflare"; import type { Readable } from "node:stream"; @@ -80,7 +81,7 @@ export function bucketAndKeyFromObjectPath(objectPath = ""): { } { const match = /^([^/]+)\/(.*)/.exec(objectPath); if (match === null) { - throw new Error( + throw new UserError( `The object path must be in the form of {bucket}/{key} you provided ${objectPath}` ); } diff --git a/packages/wrangler/src/r2/index.ts b/packages/wrangler/src/r2/index.ts index bb18778baa39..53e3d5749b05 100644 --- a/packages/wrangler/src/r2/index.ts +++ b/packages/wrangler/src/r2/index.ts @@ -5,7 +5,7 @@ import * as stream from "node:stream"; import { ReadableStream } from "node:stream/web"; import prettyBytes from "pretty-bytes"; import { readConfig } from "../config"; -import { FatalError } from "../errors"; +import { FatalError, UserError } from "../errors"; import { CommandLineArgsError, printWranglerBanner } from "../index"; import { logger } from "../logger"; import * as metrics from "../metrics"; @@ -127,7 +127,7 @@ export function r2(r2Yargs: CommonYargsArgv) { async (r2Bucket) => { const object = await r2Bucket.get(key); if (object === null) { - throw new Error("The specified key does not exist."); + throw new UserError("The specified key does not exist."); } // Note `object.body` is only valid inside this closure await stream.promises.pipeline(object.body, output); diff --git a/packages/wrangler/src/routes.ts b/packages/wrangler/src/routes.ts index 57e8c3a50bae..bac83c9d69e3 100644 --- a/packages/wrangler/src/routes.ts +++ b/packages/wrangler/src/routes.ts @@ -1,6 +1,7 @@ import chalk from "chalk"; import { fetchResult } from "./cfetch"; import { confirm, prompt } from "./dialogs"; +import { UserError } from "./errors"; import { logger } from "./logger"; export async function getWorkersDevSubdomain( @@ -30,7 +31,7 @@ export async function getWorkersDevSubdomain( "You can either deploy your worker to one or more routes by specifying them in wrangler.toml, or register a workers.dev subdomain here:"; const onboardingLink = `https://dash.cloudflare.com/${accountId}/workers/onboarding`; - throw new Error(`${solutionMessage}\n${onboardingLink}`); + throw new UserError(`${solutionMessage}\n${onboardingLink}`); } return await registerSubdomain(accountId); @@ -93,7 +94,7 @@ async function registerSubdomain(accountId: string): Promise { "You can either deploy your worker to one or more routes by specifying them in wrangler.toml, or register a workers.dev subdomain here:"; const onboardingLink = `https://dash.cloudflare.com/${accountId}/workers/onboarding`; - throw new Error(`${solutionMessage}\n${onboardingLink}`); + throw new UserError(`${solutionMessage}\n${onboardingLink}`); } try { diff --git a/packages/wrangler/src/secret/index.ts b/packages/wrangler/src/secret/index.ts index d8ce84d612df..53f5764707a1 100644 --- a/packages/wrangler/src/secret/index.ts +++ b/packages/wrangler/src/secret/index.ts @@ -8,6 +8,7 @@ import { createWorkerUploadForm, } from "../deployment-bundle/create-worker-upload-form"; import { confirm, prompt } from "../dialogs"; +import { UserError } from "../errors"; import { getLegacyScriptName, isLegacyEnv, @@ -141,7 +142,7 @@ export const secret = (secretYargs: CommonYargsArgv) => { const scriptName = getLegacyScriptName(args, config); if (!scriptName) { - throw new Error( + throw new UserError( "Required Worker name missing. Please specify the Worker name in wrangler.toml, or pass it as an argument with `--name `" ); } @@ -223,7 +224,7 @@ export const secret = (secretYargs: CommonYargsArgv) => { const scriptName = getLegacyScriptName(args, config); if (!scriptName) { - throw new Error( + throw new UserError( "Required Worker name missing. Please specify the Worker name in wrangler.toml, or pass it as an argument with `--name `" ); } @@ -273,7 +274,7 @@ export const secret = (secretYargs: CommonYargsArgv) => { const scriptName = getLegacyScriptName(args, config); if (!scriptName) { - throw new Error( + throw new UserError( "Required Worker name missing. Please specify the Worker name in wrangler.toml, or pass it as an argument with `--name `" ); } @@ -359,9 +360,11 @@ export const secretBulkHandler = async (secretBulkArgs: SecretBulkArgs) => { const scriptName = getLegacyScriptName(secretBulkArgs, config); if (!scriptName) { - throw logger.error( + const error = new UserError( "Required Worker name missing. Please specify the Worker name in wrangler.toml, or pass it as an argument with `--name `" ); + logger.error(error.message); + throw error; } const accountId = await requireAuth(config); diff --git a/packages/wrangler/src/sentry/index.ts b/packages/wrangler/src/sentry/index.ts index 56b89fdb24d5..9b366b443ed8 100644 --- a/packages/wrangler/src/sentry/index.ts +++ b/packages/wrangler/src/sentry/index.ts @@ -83,12 +83,45 @@ export const makeSentry10Transport = (options: BaseTransportOptions) => { return Sentry.createTransport(options, transportSentry10); }; +const disabledDefaultIntegrations = [ + "Console", // Console logs may contain PII + "LocalVariables", // Local variables may contain tokens and PII + "Http", // Only captures method/URL/response status, but URL may contain PII + "Undici", // Same as "Http" + "RequestData", // Request data to Wrangler's HTTP servers may contain PII +]; + export function setupSentry() { if (typeof SENTRY_DSN !== "undefined") { Sentry.init({ release: `wrangler@${wranglerVersion}`, dsn: SENTRY_DSN, transport: makeSentry10Transport, + integrations(defaultIntegrations) { + return defaultIntegrations.filter( + ({ name }) => !disabledDefaultIntegrations.includes(name) + ); + }, + beforeSend(event) { + delete event.server_name; // Computer name may contain PII + // Culture contains timezone and locale + if (event.contexts !== undefined) delete event.contexts.culture; + + // Rewrite Wrangler install location which may contain PII + const fakeInstallPath = + process.platform === "win32" ? "C:\\Project\\" : "/project/"; + for (const exception of event.exception?.values ?? []) { + for (const frame of exception.stacktrace?.frames ?? []) { + if (frame.filename === undefined) continue; + const nodeModulesIndex = frame.filename.indexOf("node_modules"); + if (nodeModulesIndex === -1) continue; + frame.filename = + fakeInstallPath + frame.filename.substring(nodeModulesIndex); + } + } + + return event; + }, }); } } diff --git a/packages/wrangler/src/sites.ts b/packages/wrangler/src/sites.ts index 5b0d918a3303..3cd2a0069cd5 100644 --- a/packages/wrangler/src/sites.ts +++ b/packages/wrangler/src/sites.ts @@ -4,6 +4,7 @@ import * as path from "node:path"; import chalk from "chalk"; import ignore from "ignore"; import xxhash from "xxhash-wasm"; +import { UserError } from "./errors"; import { createKVNamespace, listKVNamespaceKeys, @@ -405,7 +406,7 @@ async function validateAssetSize( ): Promise { const { size } = await stat(absFilePath); if (size > 25 * 1024 * 1024) { - throw new Error( + throw new UserError( `File ${relativeFilePath} is too big, it should be under 25 MiB. See https://developers.cloudflare.com/workers/platform/limits#kv-limits` ); } @@ -413,7 +414,7 @@ async function validateAssetSize( function validateAssetKey(assetKey: string) { if (assetKey.length > 512) { - throw new Error( + throw new UserError( `The asset path key "${assetKey}" exceeds the maximum key size limit of 512. See https://developers.cloudflare.com/workers/platform/limits#kv-limits",` ); } diff --git a/packages/wrangler/src/tail/filters.ts b/packages/wrangler/src/tail/filters.ts index 821877da59a8..91b4fde80818 100644 --- a/packages/wrangler/src/tail/filters.ts +++ b/packages/wrangler/src/tail/filters.ts @@ -5,6 +5,8 @@ * only recieve the ones we care about. */ +import { UserError } from "../errors"; + /** * These are the filters we accept in the CLI. They * were copied directly from Wrangler v1 in order to @@ -174,7 +176,7 @@ export function translateCLICommandToFilterMessage( */ function parseSamplingRate(sampling_rate: number): SamplingRateFilter { if (sampling_rate <= 0 || sampling_rate >= 1) { - throw new Error( + throw new UserError( "A sampling rate must be between 0 and 1 in order to have any effect.\nFor example, a sampling rate of 0.25 means 25% of events will be logged." ); } diff --git a/packages/wrangler/src/tail/index.ts b/packages/wrangler/src/tail/index.ts index a43022f30d68..6996c0964e02 100644 --- a/packages/wrangler/src/tail/index.ts +++ b/packages/wrangler/src/tail/index.ts @@ -4,6 +4,7 @@ import onExit from "signal-exit"; import { fetchResult, fetchScriptContent } from "../cfetch"; import { readConfig } from "../config"; import { confirm } from "../dialogs"; +import { UserError } from "../errors"; import { isLegacyEnv, printWranglerBanner, @@ -108,7 +109,7 @@ export async function tailHandler(args: TailArgs) { } if (!scriptName) { - throw new Error( + throw new UserError( "Required Worker name missing. Please specify the Worker name in wrangler.toml, or pass it as an argument with `wrangler tail `" ); } diff --git a/packages/wrangler/src/type-generation.ts b/packages/wrangler/src/type-generation.ts index e2c3b84a8e2d..a7f958043d44 100644 --- a/packages/wrangler/src/type-generation.ts +++ b/packages/wrangler/src/type-generation.ts @@ -1,6 +1,7 @@ import * as fs from "fs"; import { findUpSync } from "find-up"; import { getEntry } from "./deployment-bundle/entry"; +import { UserError } from "./errors"; import { logger } from "./logger"; import type { Config } from "./config"; @@ -156,7 +157,7 @@ function writeDTSFile({ .readFileSync(wranglerOverrideDTSPath, "utf8") .includes("Generated by Wrangler") ) { - throw new Error( + throw new UserError( "A non-wrangler worker-configuration.d.ts already exists, please rename and try again." ); } diff --git a/packages/wrangler/src/user/access.ts b/packages/wrangler/src/user/access.ts index f4c581dde975..4bf7c8aed6b8 100644 --- a/packages/wrangler/src/user/access.ts +++ b/packages/wrangler/src/user/access.ts @@ -1,5 +1,6 @@ import { spawnSync } from "child_process"; import { fetch } from "undici"; +import { UserError } from "../errors"; import { logger } from "../logger"; const cache: Record = {}; @@ -54,7 +55,7 @@ export async function getAccessToken( const output = spawnSync("cloudflared", ["access", "login", domain]); if (output.error) { // The cloudflared binary is not installed - throw new Error( + throw new UserError( "To use Wrangler with Cloudflare Access, please install `cloudflared` from https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation" ); } diff --git a/packages/wrangler/src/user/choose-account.tsx b/packages/wrangler/src/user/choose-account.tsx index 76cb7f336459..3786e45cf663 100644 --- a/packages/wrangler/src/user/choose-account.tsx +++ b/packages/wrangler/src/user/choose-account.tsx @@ -1,4 +1,5 @@ import { fetchPagedListResult } from "../cfetch"; +import { UserError } from "../errors"; import { getCloudflareAccountIdFromEnv } from "./auth-variables"; export type ChooseAccountItem = { @@ -20,7 +21,7 @@ export async function getAccountChoices(): Promise { }>(`/memberships`); const accounts = response.map((r) => r.account); if (accounts.length === 0) { - throw new Error( + throw new UserError( "Failed to automatically retrieve account IDs for the logged in user.\n" + "In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as `account_id` in your `wrangler.toml` file." ); @@ -29,7 +30,7 @@ export async function getAccountChoices(): Promise { } } catch (err) { if ((err as { code: number }).code === 9109) { - throw new Error( + throw new UserError( `Failed to automatically retrieve account IDs for the logged in user. You may have incorrect permissions on your API token. You can skip this account check by adding an \`account_id\` in your \`wrangler.toml\`, or by setting the value of CLOUDFLARE_ACCOUNT_ID"` ); diff --git a/packages/wrangler/src/user/user.ts b/packages/wrangler/src/user/user.ts index 78f3468d8cdf..e30b89887f59 100644 --- a/packages/wrangler/src/user/user.ts +++ b/packages/wrangler/src/user/user.ts @@ -220,6 +220,7 @@ import { saveToConfigCache, } from "../config-cache"; import { NoDefaultValueProvided, select } from "../dialogs"; +import { UserError } from "../errors"; import { getGlobalWranglerConfigPath } from "../global-wrangler-config-path"; import { CI } from "../is-ci"; import isInteractive from "../is-interactive"; @@ -452,14 +453,14 @@ interface AccessContext { * A list of OAuth2AuthCodePKCE errors. */ // To "namespace" all errors. -class ErrorOAuth2 extends Error { +class ErrorOAuth2 extends UserError { toString(): string { return "ErrorOAuth2"; } } // For really unknown errors. -class ErrorUnknown extends ErrorOAuth2 { +class ErrorUnknown extends Error { toString(): string { return "ErrorUnknown"; } @@ -1123,7 +1124,7 @@ export async function getAccountId(): Promise { } catch (e) { // Did we try to select an account in CI or a non-interactive terminal? if (e instanceof NoDefaultValueProvided) { - throw new Error( + throw new UserError( `More than one account available but unable to select one in non-interactive mode. Please set the appropriate \`account_id\` in your \`wrangler.toml\` file. Available accounts are (\`\`: \`\`): @@ -1145,17 +1146,17 @@ export async function requireAuth(config: { const loggedIn = await loginOrRefreshIfRequired(); if (!loggedIn) { if (!isInteractive() || CI.isCI()) { - throw new Error( + throw new UserError( "In a non-interactive environment, it's necessary to set a CLOUDFLARE_API_TOKEN environment variable for wrangler to work. Please go to https://developers.cloudflare.com/fundamentals/api/get-started/create-token/ for instructions on how to create an api token, and assign its value to CLOUDFLARE_API_TOKEN." ); } else { // didn't login, let's just quit - throw new Error("Did not login, quitting..."); + throw new UserError("Did not login, quitting..."); } } const accountId = config.account_id || (await getAccountId()); if (!accountId) { - throw new Error("No account id found, quitting..."); + throw new UserError("No account id found, quitting..."); } return accountId; @@ -1167,7 +1168,7 @@ export async function requireAuth(config: { export function requireApiToken(): ApiCredentials { const credentials = getAPIToken(); if (!credentials) { - throw new Error("No API token found."); + throw new UserError("No API token found."); } return credentials; } diff --git a/packages/wrangler/src/zones.ts b/packages/wrangler/src/zones.ts index 94e56b2649e7..93acfca5d4b4 100644 --- a/packages/wrangler/src/zones.ts +++ b/packages/wrangler/src/zones.ts @@ -1,4 +1,5 @@ import { fetchListResult } from "./cfetch"; +import { UserError } from "./errors"; import type { Route } from "./config/environment"; /** @@ -115,7 +116,7 @@ export async function getZoneIdFromHost(host: string): Promise { hostPieces.shift(); } - throw new Error(`Could not find zone for ${host}`); + throw new UserError(`Could not find zone for ${host}`); } /** @@ -184,7 +185,7 @@ export function findClosestRoute( export async function getWorkerForZone(worker: string) { const zone = await getZoneForRoute(worker); if (!zone) { - throw new Error( + throw new UserError( `The route '${worker}' is not part of one of your zones. Either add this zone from the Cloudflare dashboard, or try using a route within one of your existing zones.` ); } @@ -196,11 +197,11 @@ export async function getWorkerForZone(worker: string) { const closestRoute = findClosestRoute(worker, routes)?.[0]; if (!closestRoute) { - throw new Error( + throw new UserError( `The route '${worker}' has no workers assigned. You can assign a worker to it from wrangler.toml or the Cloudflare dashboard` ); } else { - throw new Error( + throw new UserError( `The route '${worker}' has no workers assigned. Did you mean to tail the route '${closestRoute.pattern}'?` ); }