diff --git a/.server-changes/sentry-wrapper-bypass-fix.md b/.server-changes/sentry-wrapper-bypass-fix.md new file mode 100644 index 0000000000..b19d90d84c --- /dev/null +++ b/.server-changes/sentry-wrapper-bypass-fix.md @@ -0,0 +1,10 @@ +--- +area: webapp +type: fix +--- + +Stop nine catch sites in the webapp from escalating expected user-input +failures (`ServiceValidationError`, `OutOfEntitlementError`, +`CreateDeclarativeScheduleError`, `QueryError`) as `error`-level events. +Type-discriminate before logging; downgrade the user-facing branches to +`warn` while keeping unknown-error fall-throughs at `error`. diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts index edaa1b257e..c22399ef60 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts @@ -60,14 +60,20 @@ export async function action({ request, params }: ActionFunctionArgs) { { status: 200 } ); } catch (e) { - logger.error("Failed to create background worker", { error: e }); - + // Customer-facing validation failures (invalid task config, customer cron + // expression, etc.). The handler returns 4xx with the message; system + // handles it gracefully, no alert needed. if (e instanceof ServiceValidationError) { + logger.warn("Failed to create background worker", { error: e.message }); return json({ error: e.message }, { status: e.status ?? 400 }); - } else if (e instanceof CreateDeclarativeScheduleError) { + } + if (e instanceof CreateDeclarativeScheduleError) { + logger.warn("Failed to create background worker", { error: e.message }); return json({ error: e.message }, { status: 400 }); } + logger.error("Failed to create background worker", { error: e }); + return json({ error: "Failed to create background worker" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts index 872ca6f2f5..bc9842f0af 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts @@ -58,14 +58,20 @@ export async function action({ request, params }: ActionFunctionArgs) { { status: 200 } ); } catch (e) { - logger.error("Failed to create background worker", { error: JSON.stringify(e) }); - + // Customer-facing validation failures (invalid task config, customer cron + // expression, etc.). The handler returns 4xx with the message; system + // handles it gracefully, no alert needed. if (e instanceof ServiceValidationError) { + logger.warn("Failed to create background worker", { error: e.message }); return json({ error: e.message }, { status: 400 }); - } else if (e instanceof CreateDeclarativeScheduleError) { + } + if (e instanceof CreateDeclarativeScheduleError) { + logger.warn("Failed to create background worker", { error: e.message }); return json({ error: e.message }, { status: 400 }); } + logger.error("Failed to create background worker", { error: e }); + return json({ error: "Failed to create background worker" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.query.ts b/apps/webapp/app/routes/api.v1.query.ts index 2250001167..05d92e9726 100644 --- a/apps/webapp/app/routes/api.v1.query.ts +++ b/apps/webapp/app/routes/api.v1.query.ts @@ -61,10 +61,16 @@ const { action, loader } = createActionApiRoute( }); if (!queryResult.success) { - const message = - queryResult.error instanceof QueryError - ? queryResult.error.message - : "An unexpected error occurred while executing the query."; + // QueryError surfaces customer SQL problems (invalid syntax, + // unsupported construct). Returned to the caller as 400; system + // handles it gracefully, no alert needed. + if (queryResult.error instanceof QueryError) { + logger.warn("Query API error", { + error: queryResult.error.message, + query, + }); + return json({ error: queryResult.error.message }, { status: 400 }); + } logger.error("Query API error", { error: queryResult.error, @@ -72,8 +78,8 @@ const { action, loader } = createActionApiRoute( }); return json( - { error: message }, - { status: queryResult.error instanceof QueryError ? 400 : 500 } + { error: "An unexpected error occurred while executing the query." }, + { status: 500 } ); } diff --git a/apps/webapp/app/routes/api.v1.tasks.batch.ts b/apps/webapp/app/routes/api.v1.tasks.batch.ts index e6ada1a739..50760b79a6 100644 --- a/apps/webapp/app/routes/api.v1.tasks.batch.ts +++ b/apps/webapp/app/routes/api.v1.tasks.batch.ts @@ -127,6 +127,18 @@ const { action, loader } = createActionApiRoute( return json(batch, { status: 202, headers: $responseHeaders }); } catch (error) { + // Customer-facing validation/quota failures (invalid batch shape, + // entitlements exhausted). The handler returns 422 with the message; + // system handles it gracefully, no alert needed. + if (error instanceof ServiceValidationError) { + logger.warn("Batch trigger error", { error: error.message }); + return json({ error: error.message }, { status: 422 }); + } + if (error instanceof OutOfEntitlementError) { + logger.warn("Batch trigger error", { error: error.message }); + return json({ error: error.message }, { status: 422 }); + } + logger.error("Batch trigger error", { error: { message: (error as Error).message, @@ -134,11 +146,7 @@ const { action, loader } = createActionApiRoute( }, }); - if (error instanceof ServiceValidationError) { - return json({ error: error.message }, { status: 422 }); - } else if (error instanceof OutOfEntitlementError) { - return json({ error: error.message }, { status: 422 }); - } else if (error instanceof Error) { + if (error instanceof Error) { return json( { error: "Something went wrong" }, { status: 500, headers: { "x-should-retry": "false" } } diff --git a/apps/webapp/app/routes/api.v2.tasks.batch.ts b/apps/webapp/app/routes/api.v2.tasks.batch.ts index 8db98b4d34..e45f7508b9 100644 --- a/apps/webapp/app/routes/api.v2.tasks.batch.ts +++ b/apps/webapp/app/routes/api.v2.tasks.batch.ts @@ -144,6 +144,18 @@ const { action, loader } = createActionApiRoute( headers: $responseHeaders, }); } catch (error) { + // Customer-facing validation/quota failures (invalid batch shape, + // entitlements exhausted). The handler returns 422 with the message; + // system handles it gracefully, no alert needed. + if (error instanceof ServiceValidationError) { + logger.warn("Batch trigger error", { error: error.message }); + return json({ error: error.message }, { status: 422 }); + } + if (error instanceof OutOfEntitlementError) { + logger.warn("Batch trigger error", { error: error.message }); + return json({ error: error.message }, { status: 422 }); + } + logger.error("Batch trigger error", { error: { message: (error as Error).message, @@ -151,11 +163,7 @@ const { action, loader } = createActionApiRoute( }, }); - if (error instanceof ServiceValidationError) { - return json({ error: error.message }, { status: 422 }); - } else if (error instanceof OutOfEntitlementError) { - return json({ error: error.message }, { status: 422 }); - } else if (error instanceof Error) { + if (error instanceof Error) { return json( { error: error.message }, { status: 500, headers: { "x-should-retry": "false" } } diff --git a/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts b/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts index 2d732d1555..0e26bae94e 100644 --- a/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts +++ b/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts @@ -88,6 +88,22 @@ export async function action({ request, params }: ActionFunctionArgs) { return json(result, { status: 200 }); } catch (error) { + // Customer-facing validation failures (invalid item shape, invalid JSON + // in the streamed body). The handler returns 4xx with the message; + // system handles it gracefully, no alert needed. + if (error instanceof ServiceValidationError) { + logger.warn("Stream batch items error", { batchId, error: error.message }); + return json({ error: error.message }, { status: 422 }); + } + + if (error instanceof Error && error.message.includes("Invalid JSON")) { + logger.warn("Stream batch items error: invalid JSON", { + batchId, + error: error.message, + }); + return json({ error: error.message }, { status: 400 }); + } + logger.error("Stream batch items error", { batchId, error: { @@ -96,14 +112,7 @@ export async function action({ request, params }: ActionFunctionArgs) { }, }); - if (error instanceof ServiceValidationError) { - return json({ error: error.message }, { status: 422 }); - } else if (error instanceof Error) { - // Check for stream parsing errors (e.g. invalid JSON) - if (error.message.includes("Invalid JSON")) { - return json({ error: error.message }, { status: 400 }); - } - + if (error instanceof Error) { return json({ error: error.message }, { status: 500 }); } diff --git a/apps/webapp/app/routes/api.v3.batches.ts b/apps/webapp/app/routes/api.v3.batches.ts index 5067eaef06..b671a8efbd 100644 --- a/apps/webapp/app/routes/api.v3.batches.ts +++ b/apps/webapp/app/routes/api.v3.batches.ts @@ -172,6 +172,18 @@ const { action, loader } = createActionApiRoute( ); } + // Customer-facing validation/quota failures (invalid batch shape, + // entitlements exhausted). The handler returns 422 with the message; + // system handles it gracefully, no alert needed. + if (error instanceof ServiceValidationError) { + logger.warn("Create batch error", { error: error.message }); + return json({ error: error.message }, { status: error.status ?? 422 }); + } + if (error instanceof OutOfEntitlementError) { + logger.warn("Create batch error", { error: error.message }); + return json({ error: error.message }, { status: 422 }); + } + logger.error("Create batch error", { error: { message: (error as Error).message, @@ -179,11 +191,7 @@ const { action, loader } = createActionApiRoute( }, }); - if (error instanceof ServiceValidationError) { - return json({ error: error.message }, { status: 422 }); - } else if (error instanceof OutOfEntitlementError) { - return json({ error: error.message }, { status: 422 }); - } else if (error instanceof Error) { + if (error instanceof Error) { return json( { error: error.message }, { status: 500, headers: { "x-should-retry": "false" } } diff --git a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts index c838132724..8f49dc34aa 100644 --- a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts @@ -146,16 +146,27 @@ export class CreateBackgroundWorkerService extends BaseService { ); if (schedulesError) { + if (schedulesError instanceof ServiceValidationError) { + // Customer schedule config (typically invalid cron). Surface to + // client via the rethrow; system returns gracefully. + logger.warn("Error syncing declarative schedules", { + error: schedulesError.message, + backgroundWorker, + environment, + }); + throw schedulesError; + } + + // Wrapping the underlying error into a ServiceValidationError below + // would otherwise hide it once the SDK-level filter drops SVEs; log at + // error so the underlying cause stays visible. Mirrors the + // waitpointCompletionPacket.server.ts pattern from dac9c83bd. logger.error("Error syncing declarative schedules", { error: schedulesError, backgroundWorker, environment, }); - if (schedulesError instanceof ServiceValidationError) { - throw schedulesError; - } - throw new ServiceValidationError("Error syncing declarative schedules"); } diff --git a/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts b/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts index cc73a8569d..16e36b57df 100644 --- a/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts +++ b/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts @@ -139,14 +139,26 @@ export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService { ); if (schedulesError) { + if (schedulesError instanceof ServiceValidationError) { + // Customer schedule config (typically invalid cron). Surface to + // client via the rethrow; system returns gracefully. + logger.warn("Error syncing declarative schedules", { + error: schedulesError.message, + }); + + await this.#failBackgroundWorkerDeployment(deployment, schedulesError); + throw schedulesError; + } + + // Wrapping the underlying error into a ServiceValidationError below + // would otherwise hide it once the SDK-level filter drops SVEs; log at + // error so the underlying cause stays visible. Mirrors the + // waitpointCompletionPacket.server.ts pattern from dac9c83bd. logger.error("Error syncing declarative schedules", { error: schedulesError, }); - const serviceError = - schedulesError instanceof ServiceValidationError - ? schedulesError - : new ServiceValidationError("Error syncing declarative schedules"); + const serviceError = new ServiceValidationError("Error syncing declarative schedules"); await this.#failBackgroundWorkerDeployment(deployment, serviceError);