diff --git a/components/gitpod-db/src/typeorm/team-db-impl.spec.db.ts b/components/gitpod-db/src/typeorm/team-db-impl.spec.db.ts index 692d5c4c2d6a24..657c39ce3e89aa 100644 --- a/components/gitpod-db/src/typeorm/team-db-impl.spec.db.ts +++ b/components/gitpod-db/src/typeorm/team-db-impl.spec.db.ts @@ -12,8 +12,7 @@ import { DBUser } from "./entity/db-user"; import * as chai from "chai"; import { TeamDB } from "../team-db"; import { DBTeam } from "./entity/db-team"; -import { ResponseError } from "vscode-jsonrpc"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; const expect = chai.expect; @suite(timeout(10000)) @@ -52,8 +51,8 @@ export class TeamDBSpec { await this.teamDB.createTeam(user.id, "X"); expect.fail("Team name too short"); } catch (error) { - if (error instanceof ResponseError && error.code === ErrorCodes.BAD_REQUEST) { - // expected ResponseError of code BAD_REQUEST + if (error instanceof ApplicationError && error.code === ErrorCodes.BAD_REQUEST) { + // expected ApplicationError of code BAD_REQUEST } else { expect.fail("Unexpected error: " + error); } @@ -65,8 +64,8 @@ export class TeamDBSpec { ); expect.fail("Team name too long"); } catch (error) { - if (error instanceof ResponseError && error.code === ErrorCodes.BAD_REQUEST) { - // expected ResponseError of code BAD_REQUEST + if (error instanceof ApplicationError && error.code === ErrorCodes.BAD_REQUEST) { + // expected ApplicationError of code BAD_REQUEST } else { expect.fail("Unexpected error: " + error); } diff --git a/components/gitpod-db/src/typeorm/team-db-impl.ts b/components/gitpod-db/src/typeorm/team-db-impl.ts index 7b7b16f8a922f3..c64f49d413d1c3 100644 --- a/components/gitpod-db/src/typeorm/team-db-impl.ts +++ b/components/gitpod-db/src/typeorm/team-db-impl.ts @@ -12,13 +12,12 @@ import { TeamMembershipInvite, User, } from "@gitpod/gitpod-protocol"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { randomBytes } from "crypto"; import { inject, injectable, optional } from "inversify"; import slugify from "slugify"; import { EntityManager, Repository } from "typeorm"; import { v4 as uuidv4 } from "uuid"; -import { ResponseError } from "vscode-jsonrpc"; import { TeamDB } from "../team-db"; import { DBTeam } from "./entity/db-team"; import { DBTeamMembership } from "./entity/db-team-membership"; @@ -144,7 +143,7 @@ export class TeamDBImpl extends TransactionalDBImpl implements TeamDB { public async updateTeam(teamId: string, team: Pick): Promise { const name = team.name && team.name.trim(); if (!name) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "No update provided"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "No update provided"); } // Storing entry in a TX to avoid potential slug dupes caused by racing requests. @@ -153,7 +152,7 @@ export class TeamDBImpl extends TransactionalDBImpl implements TeamDB { const existingTeam = await teamRepo.findOne({ id: teamId, deleted: false, markedDeleted: false }); if (!existingTeam) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Organization not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Organization not found"); } // no changes @@ -162,7 +161,10 @@ export class TeamDBImpl extends TransactionalDBImpl implements TeamDB { } if (name.length > 32) { - throw new ResponseError(ErrorCodes.INVALID_VALUE, "The name must be between 1 and 32 characters long"); + throw new ApplicationError( + ErrorCodes.INVALID_VALUE, + "The name must be between 1 and 32 characters long", + ); } existingTeam.name = name; existingTeam.slug = await this.createUniqueSlug(teamRepo, name); @@ -173,17 +175,20 @@ export class TeamDBImpl extends TransactionalDBImpl implements TeamDB { public async createTeam(userId: string, name: string): Promise { if (!name) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Name cannot be empty"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Name cannot be empty"); } name = name.trim(); if (name.length < 3) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.BAD_REQUEST, "Please choose a name that is at least three characters long.", ); } if (name.length > 64) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Please choose a name that is at most 64 characters long."); + throw new ApplicationError( + ErrorCodes.BAD_REQUEST, + "Please choose a name that is at most 64 characters long.", + ); } // Storing new entry in a TX to avoid potential dupes caused by racing requests. @@ -228,7 +233,7 @@ export class TeamDBImpl extends TransactionalDBImpl implements TeamDB { slug = slug + "-" + randomBytes(4).toString("hex"); } if (tries >= 5) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.INTERNAL_SERVER_ERROR, `Failed to create a unique slug for the '${name}'`, ); @@ -259,7 +264,7 @@ export class TeamDBImpl extends TransactionalDBImpl implements TeamDB { const teamRepo = await this.getTeamRepo(); const team = await teamRepo.findOne(teamId); if (!team || !!team.deleted) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "An organization with this ID could not be found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "An organization with this ID could not be found"); } const membershipRepo = await this.getMembershipRepo(); const membership = await membershipRepo.findOne({ teamId, userId, deleted: false }); @@ -281,7 +286,7 @@ export class TeamDBImpl extends TransactionalDBImpl implements TeamDB { const teamRepo = await this.getTeamRepo(); const team = await teamRepo.findOne(teamId); if (!team || !!team.deleted) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "An organization with this ID could not be found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "An organization with this ID could not be found"); } const membershipRepo = await this.getMembershipRepo(); @@ -292,13 +297,13 @@ export class TeamDBImpl extends TransactionalDBImpl implements TeamDB { deleted: false, }); if (ownerCount <= 1) { - throw new ResponseError(ErrorCodes.CONFLICT, "An organization must retain at least one owner"); + throw new ApplicationError(ErrorCodes.CONFLICT, "An organization must retain at least one owner"); } } const membership = await membershipRepo.findOne({ teamId, userId, deleted: false }); if (!membership) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "The user is not currently a member of this organization"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "The user is not currently a member of this organization"); } membership.role = role; await membershipRepo.save(membership); @@ -308,12 +313,12 @@ export class TeamDBImpl extends TransactionalDBImpl implements TeamDB { const teamRepo = await this.getTeamRepo(); const team = await teamRepo.findOne(teamId); if (!team || !!team.deleted) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "An organization with this ID could not be found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "An organization with this ID could not be found"); } const membershipRepo = await this.getMembershipRepo(); const membership = await membershipRepo.findOne({ teamId, userId, deleted: false }); if (!membership) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.BAD_REQUEST, "The given user is not currently a member of this organization or does not exist.", ); @@ -326,7 +331,7 @@ export class TeamDBImpl extends TransactionalDBImpl implements TeamDB { const inviteRepo = await this.getMembershipInviteRepo(); const invite = await inviteRepo.findOne(inviteId); if (!invite) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "No invite found for the given ID."); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "No invite found for the given ID."); } return invite; } diff --git a/components/gitpod-protocol/src/messaging/error.ts b/components/gitpod-protocol/src/messaging/error.ts index 5a4517dc2d3dc8..fa2caf33794de1 100644 --- a/components/gitpod-protocol/src/messaging/error.ts +++ b/components/gitpod-protocol/src/messaging/error.ts @@ -4,89 +4,116 @@ * See License.AGPL.txt in the project root for license information. */ -export namespace ErrorCodes { +import { scrubber } from "../util/scrubbing"; + +export class ApplicationError extends Error { + constructor(public readonly code: ErrorCode, message: string, public readonly data?: any) { + super(message); + this.data = scrubber.scrub(this.data, true); + } + + toJson() { + return { + code: this.code, + message: this.message, + data: this.data, + }; + } +} + +export namespace ApplicationError { + export function hasErrorCode(e: any): e is Error & { code: ErrorCode } { + return e && e.code !== undefined; + } +} + +export namespace ErrorCode { + export function isUserError(code: number | ErrorCode) { + return code >= 400 && code < 500; + } +} + +export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes]; + +export const ErrorCodes = { // 400 Unauthorized - export const BAD_REQUEST = 400; + BAD_REQUEST: 400 as const, // 401 Unauthorized - export const NOT_AUTHENTICATED = 401; + NOT_AUTHENTICATED: 401 as const, // 403 Forbidden - export const PERMISSION_DENIED = 403; + PERMISSION_DENIED: 403 as const, // 404 Not Found - export const NOT_FOUND = 404; + NOT_FOUND: 404 as const, // 409 Conflict (e.g. already existing) - export const CONFLICT = 409; + CONFLICT: 409 as const, // 411 No User - export const NEEDS_VERIFICATION = 411; + NEEDS_VERIFICATION: 411 as const, // 412 Precondition Failed - export const PRECONDITION_FAILED = 412; + PRECONDITION_FAILED: 412 as const, // 429 Too Many Requests - export const TOO_MANY_REQUESTS = 429; + TOO_MANY_REQUESTS: 429 as const, // 430 Repository not whitelisted (custom status code) - export const REPOSITORY_NOT_WHITELISTED = 430; + REPOSITORY_NOT_WHITELISTED: 430 as const, // 440 Prebuilds now always require a project (custom status code) - export const PROJECT_REQUIRED = 440; + PROJECT_REQUIRED: 440 as const, // 451 Out of credits - export const PAYMENT_SPENDING_LIMIT_REACHED = 451; + PAYMENT_SPENDING_LIMIT_REACHED: 451 as const, // 451 Error creating a subscription - export const SUBSCRIPTION_ERROR = 452; + SUBSCRIPTION_ERROR: 452 as const, // 455 Invalid cost center (custom status code) - export const INVALID_COST_CENTER = 455; + INVALID_COST_CENTER: 455 as const, // 460 Context Parse Error (custom status code) - export const CONTEXT_PARSE_ERROR = 460; + CONTEXT_PARSE_ERROR: 460 as const, // 461 Invalid gitpod yml (custom status code) - export const INVALID_GITPOD_YML = 461; + INVALID_GITPOD_YML: 461 as const, // 470 User Blocked (custom status code) - export const USER_BLOCKED = 470; + USER_BLOCKED: 470 as const, // 471 User Deleted (custom status code) - export const USER_DELETED = 471; + USER_DELETED: 471 as const, // 472 Terms Acceptance Required (custom status code) - export const USER_TERMS_ACCEPTANCE_REQUIRED = 472; + USER_TERMS_ACCEPTANCE_REQUIRED: 472 as const, // 481 Professional plan is required for this operation - export const PLAN_PROFESSIONAL_REQUIRED = 481; + PLAN_PROFESSIONAL_REQUIRED: 481 as const, // 490 Too Many Running Workspace - export const TOO_MANY_RUNNING_WORKSPACES = 490; + TOO_MANY_RUNNING_WORKSPACES: 490 as const, // 500 Internal Server Error - export const INTERNAL_SERVER_ERROR = 500; + INTERNAL_SERVER_ERROR: 500 as const, // 501 EE Feature - export const EE_FEATURE = 501; + EE_FEATURE: 501 as const, // 555 EE License Required - export const EE_LICENSE_REQUIRED = 555; + EE_LICENSE_REQUIRED: 555 as const, // 601 SaaS Feature - export const SAAS_FEATURE = 601; + SAAS_FEATURE: 601 as const, // 630 Snapshot Error - export const SNAPSHOT_ERROR = 630; + SNAPSHOT_ERROR: 630 as const, // 640 Headless logs are not available (yet) - export const HEADLESS_LOG_NOT_YET_AVAILABLE = 640; + HEADLESS_LOG_NOT_YET_AVAILABLE: 640 as const, // 650 Invalid Value - export const INVALID_VALUE = 650; - - export function isUserError(code: number): boolean { - return code >= 400 && code < 500; - } -} + INVALID_VALUE: 650 as const, +}; diff --git a/components/gitpod-protocol/src/messaging/proxy-factory.ts b/components/gitpod-protocol/src/messaging/proxy-factory.ts index b39181f965f8a5..73626fe3353ade 100644 --- a/components/gitpod-protocol/src/messaging/proxy-factory.ts +++ b/components/gitpod-protocol/src/messaging/proxy-factory.ts @@ -5,11 +5,12 @@ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ -import { MessageConnection, ResponseError } from "vscode-jsonrpc"; +import { MessageConnection } from "vscode-jsonrpc"; import { Event, Emitter } from "../util/event"; import { Disposable } from "../util/disposable"; import { ConnectionHandler } from "./handler"; import { log } from "../util/logging"; +import { ApplicationError } from "./error"; export type JsonRpcServer = Disposable & { /** @@ -142,7 +143,7 @@ export class JsonRpcProxyFactory implements ProxyHandler { try { return await this.target[method](...args); } catch (e) { - if (e instanceof ResponseError) { + if (ApplicationError.hasErrorCode(e)) { log.info(`Request ${method} unsuccessful: ${e.code}/"${e.message}"`, { method, args }); } else { log.error(`Request ${method} failed with internal server error`, e, { method, args }); diff --git a/components/gitpod-protocol/src/util/logging.ts b/components/gitpod-protocol/src/util/logging.ts index 07313b6c7c4c67..2cdbd7ba709592 100644 --- a/components/gitpod-protocol/src/util/logging.ts +++ b/components/gitpod-protocol/src/util/logging.ts @@ -14,10 +14,11 @@ let component: string | undefined; let version: string | undefined; export interface LogContext { - instanceId?: string; + organizationId?: string; sessionId?: string; userId?: string; workspaceId?: string; + instanceId?: string; } export namespace LogContext { export function from(params: { userId?: string; user?: any; request?: any }) { diff --git a/components/gitpod-protocol/src/util/tracing.ts b/components/gitpod-protocol/src/util/tracing.ts index 5811db2a4d5c77..2a50fe43c94438 100644 --- a/components/gitpod-protocol/src/util/tracing.ts +++ b/components/gitpod-protocol/src/util/tracing.ts @@ -9,7 +9,6 @@ import { TracingConfig, initTracerFromEnv } from "jaeger-client"; import { Sampler, SamplingDecision } from "./jaeger-client-types"; import { initGlobalTracer } from "opentracing"; import { injectable } from "inversify"; -import { ResponseError } from "vscode-jsonrpc"; import { log, LogContext } from "./logging"; export interface TraceContext { @@ -92,7 +91,7 @@ export namespace TraceContext { export function setJsonRPCError( ctx: TraceContext, method: string, - err: ResponseError, + err: Error & { code: number }, withStatusCode: boolean = false, ) { if (!ctx.span) { diff --git a/components/server/src/auth/verification-service.ts b/components/server/src/auth/verification-service.ts index febd14199d17a0..afb2e707706770 100644 --- a/components/server/src/auth/verification-service.ts +++ b/components/server/src/auth/verification-service.ts @@ -12,8 +12,7 @@ import { Twilio } from "twilio"; import { ServiceContext } from "twilio/lib/rest/verify/v2/service"; import { TeamDB, UserDB, WorkspaceDB } from "@gitpod/gitpod-db/lib"; import { ConfigCatClientFactory } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; -import { ResponseError } from "vscode-ws-jsonrpc"; +import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { VerificationInstance } from "twilio/lib/rest/verify/v2/service/verification"; import { v4 as uuidv4, validate as uuidValidate } from "uuid"; @@ -89,13 +88,13 @@ export class VerificationService { const isBlockedNumber = this.userDB.isBlockedPhoneNumber(phoneNumber); const usages = await this.userDB.countUsagesOfPhoneNumber(phoneNumber); if (usages > 3) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.INVALID_VALUE, "The given phone number has been used more than three times.", ); } if (await isBlockedNumber) { - throw new ResponseError(ErrorCodes.INVALID_VALUE, "The given phone number is blocked due to abuse."); + throw new ApplicationError(ErrorCodes.INVALID_VALUE, "The given phone number is blocked due to abuse."); } const verification = await this.verifyService.verifications.create({ to: phoneNumber, channel }); @@ -138,7 +137,7 @@ export class VerificationService { throw new Error("No verification service configured."); } if (!uuidValidate(verificationId)) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Verification ID must be a valid UUID"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Verification ID must be a valid UUID"); } const verification_check = await this.verifyService.verificationChecks.create({ diff --git a/components/server/src/dev/authenticator-dev.ts b/components/server/src/dev/authenticator-dev.ts index 7d30db8983ae65..dc3c4fcbfb88fa 100644 --- a/components/server/src/dev/authenticator-dev.ts +++ b/components/server/src/dev/authenticator-dev.ts @@ -8,8 +8,7 @@ import * as express from "express"; import { injectable, inject } from "inversify"; import { UserDB } from "@gitpod/gitpod-db/lib"; import { Strategy as DummyStrategy } from "passport-dummy"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; -import { ResponseError } from "vscode-jsonrpc"; +import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { Authenticator } from "../auth/authenticator"; import { AuthProvider } from "../auth/auth-provider"; import { AuthProviderInfo } from "@gitpod/gitpod-protocol"; @@ -46,7 +45,7 @@ class DummyAuthProvider implements AuthProvider { readonly strategy = new DummyStrategy(async (done) => { const maybeUser = await this.userDb.findUserById(DevData.createTestUser().id); if (!maybeUser) { - done(new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "No dev user in DB."), undefined); + done(new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "No dev user in DB."), undefined); } try { done(undefined, maybeUser); diff --git a/components/server/src/iam/iam-oidc-create-session-payload.ts b/components/server/src/iam/iam-oidc-create-session-payload.ts index fb6a09f9a859b9..f5b2515ae7b67b 100644 --- a/components/server/src/iam/iam-oidc-create-session-payload.ts +++ b/components/server/src/iam/iam-oidc-create-session-payload.ts @@ -4,18 +4,17 @@ * See License.AGPL.txt in the project root for license information. */ -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; -import { ResponseError } from "vscode-ws-jsonrpc"; +import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; export namespace OIDCCreateSessionPayload { export function validate(payload: any): OIDCCreateSessionPayload { if (typeof payload !== "object") { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "OIDC Create Session Payload is not an object."); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "OIDC Create Session Payload is not an object."); } // validate payload.idToken if (!hasField(payload, "idToken")) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.BAD_REQUEST, "OIDC Create Session Payload does not contain idToken object.", ); @@ -23,29 +22,29 @@ export namespace OIDCCreateSessionPayload { // validate payload.claims if (!hasField(payload, "claims")) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.BAD_REQUEST, "OIDC Create Session Payload does not contain claims object.", ); } if (hasEmptyField(payload.claims, "iss")) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Claim 'iss' (issuer) is missing"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Claim 'iss' (issuer) is missing"); } if (hasEmptyField(payload.claims, "sub")) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Claim 'sub' (subject) is missing"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Claim 'sub' (subject) is missing"); } if (hasEmptyField(payload.claims, "name")) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Claim 'name' is missing"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Claim 'name' is missing"); } if (hasEmptyField(payload.claims, "email")) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Claim 'email' is missing"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Claim 'email' is missing"); } if (hasEmptyField(payload, "organizationId")) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "OrganizationId is missing"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "OrganizationId is missing"); } if (hasEmptyField(payload, "oidcClientConfigId")) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "OIDC client config id missing"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "OIDC client config id missing"); } return payload as OIDCCreateSessionPayload; diff --git a/components/server/src/iam/iam-session-app.ts b/components/server/src/iam/iam-session-app.ts index 0974dd37f892e4..d06118623270ee 100644 --- a/components/server/src/iam/iam-session-app.ts +++ b/components/server/src/iam/iam-session-app.ts @@ -13,8 +13,8 @@ import { OIDCCreateSessionPayload } from "./iam-oidc-create-session-payload"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { Identity, User } from "@gitpod/gitpod-protocol"; import { BUILTIN_INSTLLATION_ADMIN_USER_ID, TeamDB } from "@gitpod/gitpod-db/lib"; -import { ResponseError } from "vscode-ws-jsonrpc"; import { reportJWTCookieIssued } from "../prometheus-metrics"; +import { ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; @injectable() export class IamSessionApp { @@ -40,7 +40,7 @@ export class IamSessionApp { res.status(200).json(result); } catch (error) { log.error("Error creating session on behalf of IAM", error); - if (error instanceof ResponseError) { + if (ApplicationError.hasErrorCode(error)) { res.status(error.code).json({ message: error.message }); return; } diff --git a/components/server/src/linkedin-service.ts b/components/server/src/linkedin-service.ts index 2b5adfab20b387..c5b429268a3001 100644 --- a/components/server/src/linkedin-service.ts +++ b/components/server/src/linkedin-service.ts @@ -4,13 +4,12 @@ * See License.AGPL.txt in the project root for license information. */ -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { LinkedInProfile, User } from "@gitpod/gitpod-protocol/lib/protocol"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { LinkedInProfileDB } from "@gitpod/gitpod-db/lib"; import { inject, injectable } from "inversify"; import fetch from "node-fetch"; -import { ResponseError } from "vscode-jsonrpc"; import { Config } from "./config"; @injectable() @@ -29,7 +28,7 @@ export class LinkedInService { private async getAccessToken(code: string) { const { clientId, clientSecret } = this.config.linkedInSecrets || {}; if (!clientId || !clientSecret) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.INTERNAL_SERVER_ERROR, "LinkedIn is not properly configured (no Client ID or Client Secret)", ); diff --git a/components/server/src/prebuilds/github-app.ts b/components/server/src/prebuilds/github-app.ts index 18b3f0899ef565..f2b08d3de4ed4b 100644 --- a/components/server/src/prebuilds/github-app.ts +++ b/components/server/src/prebuilds/github-app.ts @@ -38,8 +38,7 @@ import { asyncHandler } from "../express-util"; import { ContextParser } from "../workspace/context-parser-service"; import { HostContextProvider } from "../auth/host-context-provider"; import { RepoURL } from "../repohost"; -import { ResponseError } from "vscode-ws-jsonrpc"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ApplicationError, ErrorCode } from "@gitpod/gitpod-protocol/lib/messaging/error"; /** * GitHub app urls: @@ -678,8 +677,8 @@ export class GithubApp { function catchError(p: Promise): void { p.catch((err) => { let logger = log.error; - if (err instanceof ResponseError) { - logger = ErrorCodes.isUserError(err.code) ? log.info : log.error; + if (ApplicationError.hasErrorCode(err)) { + logger = ErrorCode.isUserError(err.code) ? log.info : log.error; } logger("Failed to handle github event", err); diff --git a/components/server/src/prebuilds/prebuild-manager.ts b/components/server/src/prebuilds/prebuild-manager.ts index 0c47ada5157a3b..3f1025b0ef7d8a 100644 --- a/components/server/src/prebuilds/prebuild-manager.ts +++ b/components/server/src/prebuilds/prebuild-manager.ts @@ -34,8 +34,7 @@ import { StopWorkspacePolicy } from "@gitpod/ws-manager/lib"; import { error } from "console"; import { IncrementalPrebuildsService } from "./incremental-prebuilds-service"; import { PrebuildRateLimiterConfig } from "../workspace/prebuild-rate-limiter"; -import { ResponseError } from "vscode-ws-jsonrpc"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { UserService } from "../user/user-service"; import { EntitlementService, MayStartWorkspaceResult } from "../billing/entitlement-service"; import { EnvVarService } from "../workspace/env-var-service"; @@ -132,10 +131,13 @@ export class PrebuildManager { try { if (user.blocked) { - throw new ResponseError(ErrorCodes.USER_BLOCKED, `Blocked users cannot start prebuilds (${user.name})`); + throw new ApplicationError( + ErrorCodes.USER_BLOCKED, + `Blocked users cannot start prebuilds (${user.name})`, + ); } if (!project) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.PROJECT_REQUIRED, `Running prebuilds without a project is no longer supported. Please add '${cloneURL}' as a project in a team.`, ); @@ -305,9 +307,13 @@ export class PrebuildManager { return; // we don't want to block workspace starts because of internal errors } if (!!result.usageLimitReachedOnCostCenter) { - throw new ResponseError(ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED, "Increase usage limit and try again.", { - attributionId: result.usageLimitReachedOnCostCenter, - }); + throw new ApplicationError( + ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED, + "Increase usage limit and try again.", + { + organizationId, + }, + ); } } diff --git a/components/server/src/prebuilds/start-prebuild-context-parser.ts b/components/server/src/prebuilds/start-prebuild-context-parser.ts index 512efeddcbabd8..70290f2ce7023a 100644 --- a/components/server/src/prebuilds/start-prebuild-context-parser.ts +++ b/components/server/src/prebuilds/start-prebuild-context-parser.ts @@ -5,9 +5,8 @@ */ import { User, WorkspaceContext, ContextURL } from "@gitpod/gitpod-protocol"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { injectable } from "inversify"; -import { ResponseError } from "vscode-ws-jsonrpc"; import { IPrefixContextParser } from "../workspace/context-parser"; @injectable() @@ -21,7 +20,7 @@ export class StartPrebuildContextParser implements IPrefixContextParser { } public async handle(user: User, prefix: string, context: WorkspaceContext): Promise { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.PROJECT_REQUIRED, `Running prebuilds without a project is no longer supported. Please add your repository as a project in a team.`, ); diff --git a/components/server/src/projects/projects-service.ts b/components/server/src/projects/projects-service.ts index 85cb120c0e73ea..a69170366574aa 100644 --- a/components/server/src/projects/projects-service.ts +++ b/components/server/src/projects/projects-service.ts @@ -22,8 +22,7 @@ import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { PartialProject, ProjectUsage } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol"; import { Config } from "../config"; import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; -import { ResponseError } from "vscode-ws-jsonrpc"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { URL } from "url"; import { Authorizer } from "../authorization/authorizer"; @@ -121,13 +120,13 @@ export class ProjectsService { installer: User, ): Promise { if (cloneUrl.length >= 1000) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Clone URL must be less than 1k characters."); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Clone URL must be less than 1k characters."); } try { new URL(cloneUrl); } catch (err) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Clone URL must be a valid URL."); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Clone URL must be a valid URL."); } const projects = await this.getProjectsByCloneUrls([cloneUrl]); diff --git a/components/server/src/user/stripe-service.ts b/components/server/src/user/stripe-service.ts index 2bea684b46cd16..25b217726db9e2 100644 --- a/components/server/src/user/stripe-service.ts +++ b/components/server/src/user/stripe-service.ts @@ -14,8 +14,7 @@ import { stripeClientRequestsCompletedDurationSeconds, } from "../prometheus-metrics"; import { BillingServiceClient, BillingServiceDefinition } from "@gitpod/usage-api/lib/usage/v1/billing.pb"; -import { ResponseError } from "vscode-ws-jsonrpc"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; @injectable() export class StripeService { @@ -53,7 +52,7 @@ export class StripeService { async getPortalUrlForAttributionId(attributionId: string, returnUrl: string): Promise { const customerId = await this.findCustomerByAttributionId(attributionId); if (!customerId) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.NOT_FOUND, `No Stripe Customer ID for attribution ID '${attributionId}'`, ); diff --git a/components/server/src/user/user-controller.ts b/components/server/src/user/user-controller.ts index 0dbfbd82b4be9a..49b9d3e9187ff7 100644 --- a/components/server/src/user/user-controller.ts +++ b/components/server/src/user/user-controller.ts @@ -24,9 +24,8 @@ import { reportJWTCookieIssued } from "../prometheus-metrics"; import { OwnerResourceGuard, ResourceAccessGuard, ScopedResourceGuard } from "../auth/resource-access"; import { OneTimeSecretServer } from "../one-time-secret-server"; import { ClientMetadata } from "../websocket/websocket-connection-manager"; -import { ResponseError } from "vscode-jsonrpc"; import * as fs from "fs/promises"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { GitpodServerImpl } from "../workspace/gitpod-server-impl"; import { WorkspaceStarter } from "../workspace/workspace-starter"; import { StopWorkspacePolicy } from "@gitpod/ws-manager/lib"; @@ -95,12 +94,12 @@ export class UserController { log.debug({ userId }, "OTS based login started."); const secret = await this.otsDb.get(req.params.key); if (!secret) { - throw new ResponseError(401, "Invalid OTS key"); + throw new ApplicationError(401, "Invalid OTS key"); } const user = await this.userDb.findUserById(userId); if (!user) { - throw new ResponseError(404, "User not found"); + throw new ApplicationError(404, "User not found"); } await verifyAndHandle(req, res, user, secret); @@ -130,7 +129,7 @@ export class UserController { try { const token = req.params.token; if (!token) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "missing token"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "missing token"); } const credentials = await this.readAdminCredentials(); credentials.validate(token); @@ -140,7 +139,7 @@ export class UserController { const user = await this.userDb.findUserById(BUILTIN_INSTLLATION_ADMIN_USER_ID); if (!user) { // We respond with NOT_AUTHENTICATED to prevent gleaning whether the user, or token are invalid. - throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Admin user not found"); + throw new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "Admin user not found"); } // Ensure admin user is owner of any Org. @@ -193,7 +192,7 @@ export class UserController { .update(user.id + this.config.session.secret) .digest("hex"); if (secretHash !== secret) { - throw new ResponseError(401, "OTS secret not verified"); + throw new ApplicationError(401, "OTS secret not verified"); } // mimick the shape of a successful login @@ -404,7 +403,7 @@ export class UserController { .catch((err) => log.warn(logCtx, "workspacePageClose: failed to track ide close signal", err)); res.sendStatus(200); } catch (e) { - if (e instanceof ResponseError) { + if (ApplicationError.hasErrorCode(e)) { res.status(e.code).send(e.message); log.warn( logCtx, @@ -612,13 +611,13 @@ export class UserController { // Credentials do not have to be present in the system, if admin level sign-in is entirely disabled. if (!credentialsFilePath) { - throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "No admin credentials"); + throw new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "No admin credentials"); } const contents = await fs.readFile(credentialsFilePath, { encoding: "utf8" }); const payload = await JSON.parse(contents); - const err = new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Invalid admin credentials."); + const err = new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "Invalid admin credentials."); if (!payload.expiresAt) { log.error("Admin credentials file does not contain expiry timestamp."); @@ -656,7 +655,7 @@ class AdminCredentials { const nowInSeconds = new Date().getTime() / 1000; if (nowInSeconds >= this.expiresAt) { log.error("Admin credentials are expired."); - throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "invalid token"); + throw new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "invalid token"); } const tokensMatch = crypto.timingSafeEqual( @@ -665,7 +664,7 @@ class AdminCredentials { ); if (!tokensMatch) { - throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "invalid token"); + throw new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "invalid token"); } } } diff --git a/components/server/src/user/user-service.ts b/components/server/src/user/user-service.ts index c143e8fcbf7615..f1e377a50f9117 100644 --- a/components/server/src/user/user-service.ts +++ b/components/server/src/user/user-service.ts @@ -15,8 +15,7 @@ import { TokenService } from "./token-service"; import { EmailAddressAlreadyTakenException, SelectAccountException } from "../auth/errors"; import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth"; import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; -import { ResponseError } from "vscode-ws-jsonrpc"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { UsageService } from "./usage-service"; export interface CreateUserParams { @@ -122,7 +121,7 @@ export class UserService { async blockUser(targetUserId: string, block: boolean): Promise { const target = await this.userDb.findUserById(targetUserId); if (!target) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "not found"); } target.blocked = !!block; @@ -277,7 +276,7 @@ export class UserService { async updateUser(userID: string, update: Partial): Promise { const user = await this.userDb.findUserById(userID); if (!user) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "User does not exist."); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "User does not exist."); } const allowedFields: (keyof User)[] = ["fullName", "additionalData"]; diff --git a/components/server/src/websocket/websocket-connection-manager.ts b/components/server/src/websocket/websocket-connection-manager.ts index d776845751727f..6fcadefd4c0869 100644 --- a/components/server/src/websocket/websocket-connection-manager.ts +++ b/components/server/src/websocket/websocket-connection-manager.ts @@ -12,7 +12,7 @@ import { RateLimiterError, User, } from "@gitpod/gitpod-protocol"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ApplicationError, ErrorCode, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { ConnectionHandler } from "@gitpod/gitpod-protocol/lib/messaging/handler"; import { JsonRpcConnectionHandler, @@ -402,13 +402,13 @@ class GitpodJsonRpcProxyFactory extends JsonRpcProxyFactory // explicitly guard against wrong method names if (!isValidFunctionName(method)) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Unknown method '${method}'`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Unknown method '${method}'`); } // access guard if (!this.accessGuard.canAccess(method)) { // logging/tracing is done in 'catch' clause - throw new ResponseError(ErrorCodes.PERMISSION_DENIED, `Request ${method} is not allowed`); + throw new ApplicationError(ErrorCodes.PERMISSION_DENIED, `Request ${method} is not allowed`); } // actual call @@ -418,13 +418,12 @@ class GitpodJsonRpcProxyFactory extends JsonRpcProxyFactory return result; } catch (e) { const traceID = span.context().toTraceId(); - - if (e instanceof ResponseError) { + if (ApplicationError.hasErrorCode(e)) { increaseApiCallCounter(method, e.code); observeAPICallsDuration(method, e.code, timer()); TraceContext.setJsonRPCError(ctx, method, e); - const severityLogger = ErrorCodes.isUserError(e.code) ? log.info : log.error; + const severityLogger = ErrorCode.isUserError(e.code) ? log.info : log.error; severityLogger( { userId }, `JSON RPC Request ${method} failed with user error: ${e.code}/"${e.message}"`, @@ -435,10 +434,11 @@ class GitpodJsonRpcProxyFactory extends JsonRpcProxyFactory message: e.message, }, ); + throw new ResponseError(e.code, e.message); } else { TraceContext.setError(ctx, e); // this is a "real" error - const err = new ResponseError( + const err = new ApplicationError( ErrorCodes.INTERNAL_SERVER_ERROR, `Internal server error. Please quote trace ID: '${traceID}' when reaching to Gitpod Support`, ); @@ -447,8 +447,8 @@ class GitpodJsonRpcProxyFactory extends JsonRpcProxyFactory TraceContext.setJsonRPCError(ctx, method, err, true); log.error({ userId }, `Request ${method} failed with internal server error`, e, { method, args }); + throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, e.message); } - throw e; } finally { span.finish(); } diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 7b50d167a3b52e..5d097575da4bdf 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -83,7 +83,7 @@ import { AdminModifyRoleOrPermissionRequest, WorkspaceAndInstance, } from "@gitpod/gitpod-protocol/lib/admin-protocol"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { Cancelable } from "@gitpod/gitpod-protocol/lib/util/cancelable"; import { log, LogContext } from "@gitpod/gitpod-protocol/lib/util/logging"; import { @@ -117,7 +117,7 @@ import * as crypto from "crypto"; import { inject, injectable } from "inversify"; import { URL } from "url"; import { v4 as uuidv4, validate as uuidValidate } from "uuid"; -import { Disposable, ResponseError } from "vscode-jsonrpc"; +import { Disposable } from "vscode-jsonrpc"; import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; import { AuthProviderService } from "../auth/auth-provider-service"; import { HostContextProvider } from "../auth/host-context-provider"; @@ -559,7 +559,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { private async guardAccess(resource: GuardedResource, op: ResourceAccessOp) { if (!(await this.resourceAccessGuard.canAccess(resource, op))) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.PERMISSION_DENIED, `operation not permitted: missing ${op} permission on ${resource.kind}`, ); @@ -593,16 +593,16 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { private async checkUser(methodName?: string, logPayload?: {}, ctx?: LogContext): Promise { // Generally, a user session is required. if (!this.userID) { - throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "User is not authenticated. Please login."); + throw new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "User is not authenticated. Please login."); } const user = await this.userDB.findUserById(this.userID); if (!user) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "User does not exist."); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "User does not exist."); } if (user.markedDeleted === true) { - throw new ResponseError(ErrorCodes.USER_DELETED, "User has been deleted."); + throw new ApplicationError(ErrorCodes.USER_DELETED, "User has been deleted."); } const userContext: LogContext = { ...ctx, @@ -631,7 +631,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { payload = { ...logPayload, ...payload }; } log.debug(userContext, `${methodName || "checkAndBlockUser"}: blocked`, payload); - throw new ResponseError(ErrorCodes.USER_BLOCKED, "You've been blocked."); + throw new ApplicationError(ErrorCodes.USER_BLOCKED, "You've been blocked."); } return user; } @@ -988,18 +988,18 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } if (!!workspace.softDeleted) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Workspace not found!"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Workspace not found!"); } // no matter if the workspace is shared or not, you cannot create a new instance await this.guardAccess({ kind: "workspaceInstance", subject: undefined, workspace }, "create"); if (workspace.type !== "regular") { - throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "Cannot (re-)start irregular workspace."); + throw new ApplicationError(ErrorCodes.PERMISSION_DENIED, "Cannot (re-)start irregular workspace."); } if (workspace.deleted) { - throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "Cannot (re-)start a deleted workspace."); + throw new ApplicationError(ErrorCodes.PERMISSION_DENIED, "Cannot (re-)start a deleted workspace."); } const envVarsPromise = this.envVarService.resolve(workspace); const projectPromise = workspace.projectId @@ -1045,7 +1045,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } catch (err) { log.error(logCtx, "stopWorkspace error: ", err); if (isClusterMaintenanceError(err)) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.PRECONDITION_FAILED, "Cannot stop the workspace because the workspace cluster is under maintenance. Please try again in a few minutes", ); @@ -1092,7 +1092,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const user = await this.checkAndBlockUser(method); if (!this.authorizationService.hasPermission(user, requiredPermission)) { log.warn({ userId: user.id }, "unauthorised admin access", { authorised: false, method, params }); - throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "not allowed"); + throw new ApplicationError(ErrorCodes.PERMISSION_DENIED, "not allowed"); } log.info({ userId: user.id }, "admin access", { authorised: true, method, params }); } @@ -1202,12 +1202,12 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { try { const wsi = await this.workspaceDb.trace(ctx).findInstanceById(instanceId); if (!wsi) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "workspace does not exist"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "workspace does not exist"); } const ws = await this.workspaceDb.trace(ctx).findById(wsi.workspaceId); if (!ws) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "workspace does not exist"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "workspace does not exist"); } await this.guardAccess({ kind: "workspaceInstance", subject: wsi, workspace: ws }, "update"); @@ -1268,7 +1268,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { private async internalGetWorkspace(user: User, id: string, db: WorkspaceDB): Promise { const workspace = await db.findById(id); if (!workspace) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Workspace not found."); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Workspace not found."); } return workspace; } @@ -1379,14 +1379,14 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { }).catch(); const snapshot = await this.workspaceDb.trace(ctx).findSnapshotById(context.snapshotId); if (!snapshot) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.NOT_FOUND, "No snapshot with id '" + context.snapshotId + "' found.", ); } const workspace = await this.workspaceDb.trace(ctx).findById(snapshot.originalWorkspaceId); if (!workspace) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.NOT_FOUND, "No workspace with id '" + snapshot.originalWorkspaceId + "' found.", ); @@ -1401,7 +1401,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { if (UnauthorizedError.is(error)) { throw error; } - throw new ResponseError( + throw new ApplicationError( ErrorCodes.PERMISSION_DENIED, `Snapshot URLs require read access to the underlying repository. Please request access from the repository owner.`, ); @@ -1530,13 +1530,13 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { private handleError(error: any, logContext: LogContext, normalizedContextUrl: string) { if (NotFoundError.is(error)) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Repository not found.", error.data); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Repository not found.", error.data); } if (UnauthorizedError.is(error)) { - throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data); + throw new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data); } if (InvalidGitpodYMLError.is(error)) { - throw new ResponseError(ErrorCodes.INVALID_GITPOD_YML, error.message); + throw new ApplicationError(ErrorCodes.INVALID_GITPOD_YML, error.message); } const errorCode = this.parseErrorCode(error); @@ -1545,7 +1545,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { throw error; } log.debug(logContext, error); - throw new ResponseError( + throw new ApplicationError( ErrorCodes.CONTEXT_PARSE_ERROR, error && error.message ? error.message : `Cannot create workspace for URL: ${normalizedContextUrl}`, ); @@ -1625,7 +1625,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const project = await this.projectsService.getProject(projectId); if (!project) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Project not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Project not found"); } await this.guardProjectOperation(user, projectId, "get"); @@ -1644,7 +1644,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const project = await this.projectsService.getProject(projectId); if (!project) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Project not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Project not found"); } await this.guardProjectOperation(user, projectId, "update"); @@ -1653,7 +1653,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { : (await this.projectsService.getBranchDetails(user, project)).filter((b) => b.isDefault); if (branchDetails.length !== 1) { log.debug({ userId: user.id }, "Cannot find branch details.", { project, branchName }); - throw new ResponseError( + throw new ApplicationError( ErrorCodes.NOT_FOUND, `Could not find ${!branchName ? "a default branch" : `branch '${branchName}'`} in repository ${ project.cloneUrl @@ -1747,15 +1747,19 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { return; // we don't want to block workspace starts because of internal errors } if (!!result.needsVerification) { - throw new ResponseError(ErrorCodes.NEEDS_VERIFICATION, `Please verify your account.`); + throw new ApplicationError(ErrorCodes.NEEDS_VERIFICATION, `Please verify your account.`); } if (!!result.usageLimitReachedOnCostCenter) { - throw new ResponseError(ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED, "Increase usage limit and try again.", { - attributionId: result.usageLimitReachedOnCostCenter, - }); + throw new ApplicationError( + ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED, + "Increase usage limit and try again.", + { + attributionId: result.usageLimitReachedOnCostCenter, + }, + ); } if (!!result.hitParallelWorkspaceLimit) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.TOO_MANY_RUNNING_WORKSPACES, `You cannot run more than ${result.hitParallelWorkspaceLimit.max} workspaces at the same time. Please stop a workspace before starting another one.`, ); @@ -1926,21 +1930,21 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const user = await this.checkUser("setWorkspaceTimeout"); if (!(await this.entitlementService.maySetTimeout(user, new Date()))) { - throw new ResponseError(ErrorCodes.PLAN_PROFESSIONAL_REQUIRED, "Plan upgrade is required"); + throw new ApplicationError(ErrorCodes.PLAN_PROFESSIONAL_REQUIRED, "Plan upgrade is required"); } let validatedDuration; try { validatedDuration = WorkspaceTimeoutDuration.validate(duration); } catch (err) { - throw new ResponseError(ErrorCodes.INVALID_VALUE, "Invalid duration : " + err.message); + throw new ApplicationError(ErrorCodes.INVALID_VALUE, "Invalid duration : " + err.message); } const workspace = await this.internalGetWorkspace(user, workspaceId, this.workspaceDb.trace(ctx)); const runningInstances = await this.workspaceDb.trace(ctx).findRegularRunningInstances(user.id); const runningInstance = runningInstances.find((i) => i.workspaceId === workspaceId); if (!runningInstance) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Can only set keep-alive for running workspaces"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Can only set keep-alive for running workspaces"); } await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspace: workspace }, "update"); @@ -1993,7 +1997,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const instance = await this.workspaceDb.trace(ctx).findRunningInstance(workspaceId); const workspace = await this.workspaceDb.trace(ctx).findById(workspaceId); if (!instance || !workspace) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} has no running instance`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} has no running instance`); } await this.guardAccess({ kind: "workspaceInstance", subject: instance, workspace }, "get"); @@ -2173,7 +2177,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const logInfo = instance.imageBuildInfo?.log; if (!logInfo) { log.error(logCtx, "cannot watch imagebuild logs for workspaceId: no image build info available"); - throw new ResponseError( + throw new ApplicationError( ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE, "cannot watch imagebuild logs for workspaceId", ); @@ -2213,7 +2217,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } catch (err) { // This error is most likely a temporary one (too early). We defer to the client whether they want to keep on trying or not. log.debug(logCtx, "cannot watch imagebuild logs for workspaceId", err); - throw new ResponseError( + throw new ApplicationError( ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE, "cannot watch imagebuild logs for workspaceId", ); @@ -2230,7 +2234,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const ws = await this.workspaceDb.trace(ctx).findByInstanceId(instanceId); if (!ws) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `Workspace ${instanceId} not found`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace ${instanceId} not found`); } const wsiPromise = this.workspaceDb.trace(ctx).findInstanceById(instanceId); @@ -2240,12 +2244,12 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const wsi = await wsiPromise; if (!wsi) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `Workspace instance for ${instanceId} not found`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace instance for ${instanceId} not found`); } const urls = await this.headlessLogService.getHeadlessLogURLs(logCtx, wsi, ws.ownerId); if (!urls || (typeof urls.streams === "object" && Object.keys(urls.streams).length === 0)) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `Headless logs for ${instanceId} not found`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `Headless logs for ${instanceId} not found`); } return urls; } @@ -2300,7 +2304,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const user = await this.checkAndBlockUser(); if (!this.config.githubApp?.enabled) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.NOT_FOUND, "No GitHub app enabled for this installation. Please talk to your administrator.", ); @@ -2320,7 +2324,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const instance = await this.workspaceDb.trace(ctx).findRunningInstance(workspaceId); if (!instance) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} has no running instance`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} has no running instance`); } await this.guardAccess({ kind: "workspaceInstance", subject: instance, workspace }, "get"); @@ -2336,7 +2340,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { snapshotUrl = resp.getUrl(); } catch (err) { if (isClusterMaintenanceError(err)) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.PRECONDITION_FAILED, "Cannot take a snapshot because the workspace cluster is under maintenance. Please try again in a few minutes", ); @@ -2363,7 +2367,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { /** * @param snapshotId - * @throws ResponseError with either NOT_FOUND or SNAPSHOT_ERROR in case the snapshot is not done yet. + * @throws ApplicationError with either NOT_FOUND or SNAPSHOT_ERROR in case the snapshot is not done yet. */ async waitForSnapshot(ctx: TraceContext, snapshotId: string): Promise { traceAPIParams(ctx, { snapshotId }); @@ -2372,7 +2376,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const snapshot = await this.workspaceDb.trace(ctx).findSnapshotById(snapshotId); if (!snapshot) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `No snapshot with id '${snapshotId}' found.`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `No snapshot with id '${snapshotId}' found.`); } const snapshotWorkspace = await this.guardSnaphotAccess(ctx, user.id, snapshot.originalWorkspaceId); await this.internalDoWaitForWorkspace({ workspaceOwner: snapshotWorkspace.ownerId, snapshot }); @@ -2386,7 +2390,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const workspace = await this.workspaceDb.trace(ctx).findById(workspaceId); if (!workspace || workspace.ownerId !== user.id) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} does not exist.`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} does not exist.`); } const snapshots = await this.workspaceDb.trace(ctx).findSnapshotsByWorkspaceId(workspaceId); @@ -2400,7 +2404,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { await this.snapshotService.waitForSnapshot(opts); } catch (err) { // wrap in SNAPSHOT_ERROR to signal this call should not be retried. - throw new ResponseError(ErrorCodes.SNAPSHOT_ERROR, err.toString()); + throw new ApplicationError(ErrorCodes.SNAPSHOT_ERROR, err.toString()); } } @@ -2451,7 +2455,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { // validate input const validationError = UserEnvVar.validate(variable); if (validationError) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, validationError); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, validationError); } variable.repositoryPattern = UserEnvVar.normalizeRepoPattern(variable.repositoryPattern); @@ -2469,7 +2473,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { // this is a new variable - make sure the user does not have too many (don't DOS our database using gp env) const varCount = existingVars.length; if (varCount > this.config.maxEnvvarPerUserCount) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.PERMISSION_DENIED, `cannot have more than ${this.config.maxEnvvarPerUserCount} environment variables`, ); @@ -2509,7 +2513,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } if (!variable.id) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.NOT_FOUND, `cannot delete '${variable.name}' in scope '${variable.repositoryPattern}'`, ); @@ -2608,7 +2612,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const user = await this.checkAndBlockUser("deleteProjectEnvironmentVariable"); const envVar = await this.projectsService.getProjectEnvironmentVariableById(variableId); if (!envVar) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Project environment variable not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Project environment variable not found"); } await this.guardProjectOperation(user, envVar.projectId, "update"); return this.projectsService.deleteProjectEnvironmentVariable(envVar.id); @@ -2619,7 +2623,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const workspace = await this.workspaceDb.trace(ctx).findById(workspaceId); if (!workspace || workspace.ownerId !== userId) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} does not exist.`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} does not exist.`); } await this.guardAccess({ kind: "snapshot", subject: undefined, workspace }, "create"); @@ -2632,12 +2636,12 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { fineGrainedOp: OrganizationPermission | "not_implemented", ): Promise<{ team: Team; members: TeamMemberInfo[] }> { if (!uuidValidate(teamId)) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "organization ID must be a valid UUID"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organization ID must be a valid UUID"); } const team = await this.teamDB.findTeamById(teamId); if (!team) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `Organization ID: ${teamId} not found.`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `Organization ID: ${teamId} not found.`); } const members = await this.teamDB.findMembersByTeam(team.id); @@ -2645,9 +2649,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { if (!(await this.hasOrgOperationPermission(team, members, op, fineGrainedOp))) { // if user has read permission, throw 403, otherwise 404 if (await this.hasOrgOperationPermission(team, members, "get", "read_info")) { - throw new ResponseError(ErrorCodes.PERMISSION_DENIED, `No access to Organization ID: ${teamId}`); + throw new ApplicationError(ErrorCodes.PERMISSION_DENIED, `No access to Organization ID: ${teamId}`); } else { - throw new ResponseError(ErrorCodes.NOT_FOUND, `Organization ID: ${teamId} not found.`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `Organization ID: ${teamId} not found.`); } } return { team, members }; @@ -2753,7 +2757,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const mayCreateOrganization = await this.userService.mayCreateOrJoinOrganization(user); if (!mayCreateOrganization) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.PERMISSION_DENIED, "Organizational accounts are not allowed to create new organizations", ); @@ -2802,7 +2806,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const mayCreateOrganization = await this.userService.mayCreateOrJoinOrganization(user); if (!mayCreateOrganization) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.PERMISSION_DENIED, "Organizational accounts are not allowed to join other organizations", ); @@ -2811,11 +2815,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { // Invites can be used by anyone, as long as they know the invite ID, hence needs no resource guard const invite = await this.teamDB.findTeamMembershipInviteById(inviteId); if (!invite || invite.invalidationTime !== "") { - throw new ResponseError(ErrorCodes.NOT_FOUND, "The invite link is no longer valid."); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "The invite link is no longer valid."); } ctx.span?.setTag("teamId", invite.teamId); if (await this.teamDB.hasActiveSSO(invite.teamId)) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Invites are disabled for SSO-enabled organizations."); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Invites are disabled for SSO-enabled organizations."); } const result = await this.teamDB.addMemberToTeam(user.id, invite.teamId); const org = await this.getTeam(ctx, invite.teamId); @@ -2857,11 +2861,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { traceAPIParams(ctx, { teamId, userId, role }); if (!uuidValidate(userId)) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "user ID must be a valid UUID"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "user ID must be a valid UUID"); } if (!TeamMemberRole.isValid(role)) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "invalid role name"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "invalid role name"); } const { team } = await this.guardTeamOperation(teamId, "update", "write_members"); @@ -2881,7 +2885,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { traceAPIParams(ctx, { teamId: orgID, userId }); if (!uuidValidate(userId)) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "user ID must be a valid UUID"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "user ID must be a valid UUID"); } const requestor = await this.checkAndBlockUser("removeTeamMember"); @@ -2897,7 +2901,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { // Check for existing membership. const membership = await this.teamDB.findTeamMembership(userId, orgID); if (!membership) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.NOT_FOUND, `Could not find membership for user '${userId}' in organization '${orgID}'`, ); @@ -2906,11 +2910,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { // Check if user's account belongs to the Org. const userToBeRemoved = currentUserLeavingTeam ? requestor : await this.userDB.findUserById(userId); if (!userToBeRemoved) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `Could not find user '${userId}'`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `Could not find user '${userId}'`); } // Only invited members can be removed from the Org, but organizational accounts cannot. if (userToBeRemoved.organizationId && orgID === userToBeRemoved.organizationId) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.PRECONDITION_FAILED, `User's account '${userId}' belongs to the organization '${orgID}'`, ); @@ -2943,7 +2947,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { await this.guardTeamOperation(teamId, "get", "write_members"); if (await this.teamDB.hasActiveSSO(teamId)) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Invites are disabled for SSO-enabled organizations."); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Invites are disabled for SSO-enabled organizations."); } const invite = await this.teamDB.findGenericInviteByTeamId(teamId); @@ -2959,7 +2963,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { await this.checkAndBlockUser("resetGenericInvite"); await this.guardTeamOperation(teamId, "update", "write_members"); if (await this.teamDB.hasActiveSSO(teamId)) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Invites are disabled for SSO-enabled organizations."); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Invites are disabled for SSO-enabled organizations."); } return this.teamDB.resetGenericInvite(teamId); } @@ -2967,13 +2971,13 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { private async guardProjectOperation(user: User, projectId: string, op: ResourceAccessOp): Promise { const project = await this.projectsService.getProject(projectId); if (!project) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Project not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Project not found"); } // TODO(janx): This if/else block should probably become a GuardedProject. if (project.userId) { if (user.id !== project.userId) { // Projects owned by a single user can only be accessed by that user - throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "Not allowed to access this project"); + throw new ApplicationError(ErrorCodes.PERMISSION_DENIED, "Not allowed to access this project"); } } else { // Anyone who can read a team's information (i.e. any team member) can manage team projects @@ -3131,7 +3135,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const user = await this.checkAndBlockUser("getProjectOverview"); const project = await this.projectsService.getProject(projectId); if (!project) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Project not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Project not found"); } await this.guardProjectOperation(user, projectId, "get"); try { @@ -3142,7 +3146,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { return result; } catch (error) { if (UnauthorizedError.is(error)) { - throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data); + throw new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data); } throw error; } @@ -3162,13 +3166,13 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const project = await this.projectsService.getProject(projectId); if (!project) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Project not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Project not found"); } await this.guardProjectOperation(user, projectId, "update"); const prebuild = await this.workspaceDb.trace(ctx).findPrebuildByID(prebuildId); if (!prebuild) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Prebuild not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Prebuild not found"); } // Explicitly stopping the prebuild workspace now automaticaly cancels the prebuild await this.stopWorkspace(ctx, prebuild.buildWorkspaceId); @@ -3187,13 +3191,13 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { try { url = new URL(params.cloneUrl); } catch (err) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Clone URL must be a valid URL."); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Clone URL must be a valid URL."); } const availableRepositories = await this.getProviderRepositoriesForUser(ctx, { provider: url.host }); if (!availableRepositories.some((r) => r.cloneUrl === params.cloneUrl)) { // The error message is derived from internals of `getProviderRepositoriesForUser` and // `getRepositoriesForAutomatedPrebuilds`, which require admin permissions to be present. - throw new ResponseError( + throw new ApplicationError( ErrorCodes.BAD_REQUEST, "Repository URL seems to be inaccessible, or admin permissions are missing.", ); @@ -3323,11 +3327,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { try { result = await this.userDB.findUserById(userId); } catch (e) { - throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); } if (!result) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "not found"); } return result; } @@ -3347,7 +3351,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { ); return res; } catch (e) { - throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); } } @@ -3369,7 +3373,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { ); return res; } catch (e) { - throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); } } @@ -3423,7 +3427,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { try { await this.userDeletionService.deleteUser(userId); } catch (e) { - throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); } } @@ -3432,13 +3436,13 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { try { const user = await this.userDB.findUserById(userId); if (!user) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `No user with id ${userId} found.`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `No user with id ${userId} found.`); } this.verificationService.markVerified(user); await this.userDB.updateUserPartial(user); return user; } catch (e) { - throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); } } @@ -3449,7 +3453,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const target = await this.userDB.findUserById(req.id); if (!target) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "not found"); } const rolesOrPermissions = new Set((target.rolesOrPermissions || []) as string[]); @@ -3474,7 +3478,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { await this.guardAdminAccess("adminModifyPermanentWorkspaceFeatureFlag", { req }, Permission.ADMIN_USERS); const target = await this.userDB.findUserById(req.id); if (!target) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "not found"); } const featureSettings: UserFeatureSettings = target.featureFlags || {}; @@ -3519,7 +3523,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const result = await this.workspaceDb.trace(ctx).findWorkspaceAndInstance(workspaceId); if (!result) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "not found"); } return result; } @@ -3556,13 +3560,16 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { await this.workspaceDb.trace(ctx).transaction(async (db) => { const ws = await db.findById(workspaceId); if (!ws) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `No workspace with id '${workspaceId}' found.`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `No workspace with id '${workspaceId}' found.`); } if (!ws.softDeleted) { return; } if (!!ws.contentDeletedTime) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "The workspace content was already garbage-collected."); + throw new ApplicationError( + ErrorCodes.NOT_FOUND, + "The workspace content was already garbage-collected.", + ); } // @ts-ignore ws.softDeleted = null; @@ -3613,7 +3620,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const team = await this.teamDB.findTeamById(teamId); if (!team) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Team not found"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Team not found"); } const members = await this.teamDB.findMembersByTeam(team.id); return members; @@ -3644,7 +3651,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const user = await this.checkAndBlockUser("updateOwnAuthProvider"); if (user.id !== entry.ownerId) { - throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "Not allowed to modify this resource."); + throw new ApplicationError(ErrorCodes.PERMISSION_DENIED, "Not allowed to modify this resource."); } const safeProvider = this.redactUpdateOwnAuthProviderParams({ entry }); @@ -3670,7 +3677,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { return AuthProviderEntry.redact(result); } catch (error) { const message = error && error.message ? error.message : "Failed to update the provider."; - throw new ResponseError(ErrorCodes.CONFLICT, message); + throw new ApplicationError(ErrorCodes.CONFLICT, message); } } private redactUpdateOwnAuthProviderParams({ entry }: GitpodServer.UpdateOwnAuthProviderParams) { @@ -3699,13 +3706,13 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const ownProviders = await this.authProviderService.getAuthProvidersOfUser(user.id); const authProvider = ownProviders.find((p) => p.id === params.id); if (!authProvider) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "User resource not found."); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "User resource not found."); } try { await this.authProviderService.deleteAuthProvider(authProvider); } catch (error) { const message = error && error.message ? error.message : "Failed to delete the provider."; - throw new ResponseError(ErrorCodes.CONFLICT, message); + throw new ApplicationError(ErrorCodes.CONFLICT, message); } } @@ -3728,7 +3735,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { }; if (!newProvider.organizationId || !uuidValidate(newProvider.organizationId)) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Invalid organizationId"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid organizationId"); } await this.guardWithFeatureFlag("orgGitAuthProviders", user, newProvider.organizationId); @@ -3736,7 +3743,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { await this.guardTeamOperation(newProvider.organizationId, "update", "write_git_provider"); if (!newProvider.host) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.BAD_REQUEST, "Must provider a host value when creating a new auth provider.", ); @@ -3762,7 +3769,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { return AuthProviderEntry.redact(result); } catch (error) { const message = error && error.message ? error.message : "Failed to create the provider."; - throw new ResponseError(ErrorCodes.CONFLICT, message); + throw new ApplicationError(ErrorCodes.CONFLICT, message); } } @@ -3783,7 +3790,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { }; if (!providerUpdate.organizationId || !uuidValidate(providerUpdate.organizationId)) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Invalid organizationId"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid organizationId"); } await this.guardWithFeatureFlag("orgGitAuthProviders", user, providerUpdate.organizationId); @@ -3795,7 +3802,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { return AuthProviderEntry.redact(result); } catch (error) { const message = error && error.message ? error.message : "Failed to update the provider."; - throw new ResponseError(ErrorCodes.CONFLICT, message); + throw new ApplicationError(ErrorCodes.CONFLICT, message); } } @@ -3817,7 +3824,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } catch (error) { const message = error && error.message ? error.message : "Error retreiving auth providers for organization."; - throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, message); + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, message); } } @@ -3828,7 +3835,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const team = await this.getTeam(ctx, params.organizationId); if (!team) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Invalid organizationId"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid organizationId"); } await this.guardWithFeatureFlag("orgGitAuthProviders", user, team.id); @@ -3839,14 +3846,14 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const orgProviders = await this.authProviderService.getAuthProvidersOfOrg(team.id); const authProvider = orgProviders.find((p) => p.id === params.id && p.organizationId === params.organizationId); if (!authProvider) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Provider resource not found."); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Provider resource not found."); } try { await this.authProviderService.deleteAuthProvider(authProvider); } catch (error) { const message = error && error.message ? error.message : "Failed to delete the provider."; - throw new ResponseError(ErrorCodes.CONFLICT, message); + throw new ApplicationError(ErrorCodes.CONFLICT, message); } } @@ -3880,7 +3887,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { teamId, }); if (!isEnabled) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Method not available"); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Method not available"); } } @@ -3985,7 +3992,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { await this.checkAndBlockUser("getLinkedInClientID"); const clientId = this.config.linkedInSecrets?.clientId; if (!clientId) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.INTERNAL_SERVER_ERROR, "LinkedIn is not properly configured (no Client ID)", ); @@ -4024,9 +4031,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { switch (err.code) { case grpc.status.RESOURCE_EXHAUSTED: - return new ResponseError(ErrorCodes.TOO_MANY_REQUESTS, err.details); + return new ApplicationError(ErrorCodes.TOO_MANY_REQUESTS, err.details); default: - return new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, err.details); + return new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, err.details); } } @@ -4086,7 +4093,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { lvlmap.set("owner", AdmissionLevel.ADMIT_OWNER_ONLY); lvlmap.set("everyone", AdmissionLevel.ADMIT_EVERYONE); if (!lvlmap.has(level)) { - throw new ResponseError(ErrorCodes.NOT_FOUND, "Invalid admission level."); + throw new ApplicationError(ErrorCodes.NOT_FOUND, "Invalid admission level."); } const workspace = await this.internalGetWorkspace(user, workspaceId, this.workspaceDb.trace(ctx)); @@ -4095,7 +4102,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { if (level != "owner" && workspace.organizationId) { const settings = await this.teamDB.findOrgSettings(workspace.organizationId); if (settings?.workspaceSharingDisabled) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.PERMISSION_DENIED, "An Organization Owner has disabled workspace sharing for workspaces in this Organization. ", ); @@ -4124,7 +4131,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { await this.checkAndBlockUser("getStripePublishableKey"); const publishableKey = this.config.stripeSecrets?.publishableKey; if (!publishableKey) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.INTERNAL_SERVER_ERROR, "Stripe is not properly configured (no publishable key)", ); @@ -4136,7 +4143,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const attrId = AttributionId.parse(attributionId); if (attrId === undefined) { log.error(`Invalid attribution id: ${attributionId}`); - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); } try { @@ -4145,7 +4152,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { return subscriptionId; } catch (error) { log.error(`Failed to get Stripe Subscription ID for '${attributionId}'`, error); - throw new ResponseError( + throw new ApplicationError( ErrorCodes.INTERNAL_SERVER_ERROR, `Failed to get Stripe Subscription ID for '${attributionId}'`, ); @@ -4155,7 +4162,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { async getPriceInformation(ctx: TraceContext, attributionId: string): Promise { const attrId = AttributionId.parse(attributionId); if (!attrId) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attributionId '${attributionId}'`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Invalid attributionId '${attributionId}'`); } await this.guardTeamOperation(attrId.teamId, "update", "write_billing"); @@ -4166,7 +4173,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const user = await this.checkAndBlockUser("createStripeCustomerIfNeeded"); const attrId = AttributionId.parse(attributionId); if (!attrId) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attributionId '${attributionId}'`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Invalid attributionId '${attributionId}'`); } const org = (await this.guardTeamOperation(attrId.teamId, "update", "write_billing")).team; @@ -4185,7 +4192,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { // NOTE: this is a temporary workaround, as long as we're not automatically re-create the customer // entity on Stripe to support a switch of currencies, we're taking an exit here. if (customer.currency && customer.currency !== currency) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.SUBSCRIPTION_ERROR, `Your previous subscription was in ${customer.currency}. If you'd like to change currencies, please contact our support.`, { hint: "currency", oldValue: customer.currency, value: currency }, @@ -4207,7 +4214,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { return; } catch (error) { log.error(`Failed to create Stripe customer profile for '${attributionId}'`, error); - throw new ResponseError( + throw new ApplicationError( ErrorCodes.INTERNAL_SERVER_ERROR, `Failed to create Stripe customer profile for '${attributionId}'`, ); @@ -4223,7 +4230,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const attrId = AttributionId.parse(attributionId); if (attrId === undefined) { log.error(`Invalid attribution id`); - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); } await this.guardTeamOperation(attrId.teamId, "update", "write_billing"); @@ -4237,9 +4244,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } catch (error) { log.error(`Failed to subscribe '${attributionId}' to Stripe`, error); if (error instanceof ClientError) { - throw new ResponseError(error.code, error.details); + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, error.details); } - throw new ResponseError( + throw new ApplicationError( ErrorCodes.INTERNAL_SERVER_ERROR, `Failed to subscribe '${attributionId}' to Stripe`, ); @@ -4255,7 +4262,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const attrId = AttributionId.parse(attributionId); if (attrId === undefined) { log.error(`Invalid attribution id: ${attributionId}`); - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); } try { @@ -4294,9 +4301,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } catch (error) { log.error(`Failed to subscribe '${attributionId}' to Stripe`, error); if (error instanceof ClientError) { - throw new ResponseError(error.code, error.details); + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, error.details); } - throw new ResponseError( + throw new ApplicationError( ErrorCodes.INTERNAL_SERVER_ERROR, `Failed to subscribe '${attributionId}' to Stripe`, ); @@ -4309,7 +4316,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const attrId = AttributionId.parse(attributionId); if (attrId === undefined) { log.error(`Invalid attribution id: ${attributionId}`); - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); } let returnUrl = this.config.hostUrl @@ -4321,7 +4328,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { url = await this.stripeService.getPortalUrlForAttributionId(attributionId, returnUrl); } catch (error) { log.error(`Failed to get Stripe portal URL for '${attributionId}'`, error); - throw new ResponseError( + throw new ApplicationError( ErrorCodes.INTERNAL_SERVER_ERROR, `Failed to get Stripe portal URL for '${attributionId}'`, ); @@ -4333,7 +4340,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const attrId = AttributionId.parse(attributionId); if (attrId === undefined) { log.error(`Invalid attribution id: ${attributionId}`); - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); } await this.checkAndBlockUser("getCostCenter"); @@ -4359,10 +4366,10 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const attrId = AttributionId.parse(attributionId); if (attrId === undefined) { log.error(`Invalid attribution id: ${attributionId}`); - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); } if (typeof usageLimit !== "number" || usageLimit < 0) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Unexpected usageLimit value: ${usageLimit}`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Unexpected usageLimit value: ${usageLimit}`); } await this.checkAndBlockUser("setUsageLimit"); await this.guardCostCenterAccess(ctx, attrId, "update", "write_billing"); @@ -4378,7 +4385,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } if (response.costCenter?.billingStrategy !== CostCenter_BillingStrategy.BILLING_STRATEGY_STRIPE) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.BAD_REQUEST, `Setting a usage limit is not valid for non-Stripe billing strategies`, ); @@ -4395,7 +4402,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { async listUsage(ctx: TraceContext, req: ListUsageRequest): Promise { const attributionId = AttributionId.parse(req.attributionId); if (!attributionId) { - throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "Bad attribution ID", { + throw new ApplicationError(ErrorCodes.INVALID_COST_CENTER, "Bad attribution ID", { attributionId: req.attributionId, }); } @@ -4408,7 +4415,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { await this.checkAndBlockUser("listUsage"); const parsedAttributionId = AttributionId.parse(attributionId); if (!parsedAttributionId) { - throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "Bad attribution ID", { + throw new ApplicationError(ErrorCodes.INVALID_COST_CENTER, "Bad attribution ID", { attributionId, }); } @@ -4421,7 +4428,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const { from, to } = req; const attributionId = AttributionId.parse(req.attributionId); if (!attributionId) { - throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "Bad attribution ID", { + throw new ApplicationError(ErrorCodes.INVALID_COST_CENTER, "Bad attribution ID", { attributionId: req.attributionId, }); } @@ -4469,7 +4476,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { fineGrainedOp: OrganizationPermission | "not_implemented", ): Promise<{ team: Team; members: TeamMemberInfo[] }> { if (attributionId.kind !== "team") { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Invalid attributionId"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid attributionId"); } return await this.guardTeamOperation(attributionId.teamId, operation, fineGrainedOp); @@ -4497,12 +4504,12 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const user = await this.checkAndBlockUser("adminGetBillingMode"); if (!this.authorizationService.hasPermission(user, Permission.ADMIN_USERS)) { - throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "not allowed"); + throw new ApplicationError(ErrorCodes.PERMISSION_DENIED, "not allowed"); } const parsedAttributionId = AttributionId.parse(attributionId); if (!parsedAttributionId) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Unable to parse attributionId"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Unable to parse attributionId"); } return this.billingModes.getBillingMode(parsedAttributionId, new Date()); } @@ -4511,7 +4518,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const attrId = AttributionId.parse(attributionId); if (attrId === undefined) { log.error(`Invalid attribution id: ${attributionId}`); - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); } const user = await this.checkAndBlockUser("adminGetCostCenter"); @@ -4525,10 +4532,10 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const attrId = AttributionId.parse(attributionId); if (attrId === undefined) { log.error(`Invalid attribution id: ${attributionId}`); - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); } if (typeof usageLimit !== "number" || usageLimit < 0) { - throw new ResponseError(ErrorCodes.BAD_REQUEST, `Unexpected usageLimit value: ${usageLimit}`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Unexpected usageLimit value: ${usageLimit}`); } const user = await this.checkAndBlockUser("adminSetUsageLimit"); await this.guardAdminAccess("adminSetUsageLimit", { id: user.id }, Permission.ADMIN_USERS); @@ -4537,7 +4544,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { // backward compatibility for cost centers that were created before introduction of BillingStrategy if (!response.costCenter) { - throw new ResponseError(ErrorCodes.NOT_FOUND, `Couldn't find cost center with id ${attributionId}`); + throw new ApplicationError(ErrorCodes.NOT_FOUND, `Couldn't find cost center with id ${attributionId}`); } const stripeSubscriptionId = await this.stripeService.findUncancelledSubscriptionByAttributionId(attributionId); if (stripeSubscriptionId != undefined) { diff --git a/components/server/src/workspace/workspace-factory.ts b/components/server/src/workspace/workspace-factory.ts index 5fe689322ca137..5e0fbda4dafb65 100644 --- a/components/server/src/workspace/workspace-factory.ts +++ b/components/server/src/workspace/workspace-factory.ts @@ -22,12 +22,11 @@ import { Workspace, WorkspaceContext, } from "@gitpod/gitpod-protocol"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { generateWorkspaceID } from "@gitpod/gitpod-protocol/lib/util/generate-workspace-id"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import { inject, injectable } from "inversify"; -import { ResponseError } from "vscode-jsonrpc"; import { RepoURL } from "../repohost"; import { ConfigProvider } from "./config-provider"; import { ImageSourceProvider } from "./image-source-provider"; @@ -315,7 +314,7 @@ export class WorkspaceFactory { try { const snapshot = await this.db.trace({ span }).findSnapshotById(context.snapshotId); if (!snapshot) { - throw new ResponseError( + throw new ApplicationError( ErrorCodes.NOT_FOUND, "No snapshot with id '" + context.snapshotId + "' found.", );