Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions components/gitpod-db/src/typeorm/team-db-impl.spec.db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
37 changes: 21 additions & 16 deletions components/gitpod-db/src/typeorm/team-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -144,7 +143,7 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> implements TeamDB {
public async updateTeam(teamId: string, team: Pick<Team, "name">): Promise<Team> {
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.
Expand All @@ -153,7 +152,7 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> 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
Expand All @@ -162,7 +161,10 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> 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);
Expand All @@ -173,17 +175,20 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> implements TeamDB {

public async createTeam(userId: string, name: string): Promise<Team> {
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.
Expand Down Expand Up @@ -228,7 +233,7 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> 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}'`,
);
Expand Down Expand Up @@ -259,7 +264,7 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> 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 });
Expand All @@ -281,7 +286,7 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> 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();

Expand All @@ -292,13 +297,13 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> 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);
Expand All @@ -308,12 +313,12 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> 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.",
);
Expand All @@ -326,7 +331,7 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> 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;
}
Expand Down
93 changes: 60 additions & 33 deletions components/gitpod-protocol/src/messaging/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
5 changes: 3 additions & 2 deletions components/gitpod-protocol/src/messaging/proxy-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Client> = Disposable & {
/**
Expand Down Expand Up @@ -142,7 +143,7 @@ export class JsonRpcProxyFactory<T extends object> implements ProxyHandler<T> {
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 });
Expand Down
3 changes: 2 additions & 1 deletion components/gitpod-protocol/src/util/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down
3 changes: 1 addition & 2 deletions components/gitpod-protocol/src/util/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -92,7 +91,7 @@ export namespace TraceContext {
export function setJsonRPCError(
ctx: TraceContext,
method: string,
err: ResponseError<any>,
err: Error & { code: number },
withStatusCode: boolean = false,
) {
if (!ctx.span) {
Expand Down
Loading