Skip to content

Commit 84c824e

Browse files
committed
[dashboard/server] app error conversion based on error details
1 parent 6933bab commit 84c824e

File tree

12 files changed

+660
-191
lines changed

12 files changed

+660
-191
lines changed

components/dashboard/src/components/UsageLimitReachedModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Alert from "./Alert";
99
import Modal from "./Modal";
1010
import { Heading2 } from "./typography/headings";
1111

12-
export function UsageLimitReachedModal(p: { hints: any; onClose?: () => void }) {
12+
export function UsageLimitReachedModal(p: { onClose?: () => void }) {
1313
const currentOrg = useCurrentOrg();
1414

1515
const orgName = currentOrg.data?.name;

components/dashboard/src/start/StartPage.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,7 @@ export function StartPage(props: StartPageProps) {
111111
<ProgressBar phase={phase} error={!!error} />
112112
)}
113113
{error && error.code === ErrorCodes.NEEDS_VERIFICATION && <VerifyModal />}
114-
{error && error.code === ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED && (
115-
<UsageLimitReachedModal hints={error?.data} />
116-
)}
114+
{error && error.code === ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED && <UsageLimitReachedModal />}
117115
{error && <StartError error={error} />}
118116
{props.children}
119117
<WarningView

components/dashboard/src/workspaces/CreateWorkspacePage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ const ErrorMessage: FunctionComponent<ErrorMessageProps> = ({ error, reset, setS
521521
case ErrorCodes.INVALID_COST_CENTER:
522522
return <RepositoryInputError title={`The organization '${error.data}' is not valid.`} />;
523523
case ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED:
524-
return <UsageLimitReachedModal onClose={reset} hints={error?.data} />;
524+
return <UsageLimitReachedModal onClose={reset} />;
525525
case ErrorCodes.NEEDS_VERIFICATION:
526526
return <VerifyModal />;
527527
default:

components/gitpod-protocol/src/messaging/error.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@
55
*/
66

77
import { scrubber } from "../util/scrubbing";
8+
import { PlainMessage } from "@bufbuild/protobuf";
9+
import {
10+
InvalidGitpodYMLError as InvalidGitpodYMLErrorData,
11+
RepositoryNotFoundError as RepositoryNotFoundErrorData,
12+
RepositoryUnauthorizedError as RepositoryUnauthorizedErrorData,
13+
} from "@gitpod/public-api/lib/gitpod/v1/error_pb";
814

915
export class ApplicationError extends Error {
10-
constructor(public readonly code: ErrorCode, message: string, public readonly data?: any) {
16+
constructor(readonly code: ErrorCode, readonly message: string, readonly data?: any) {
1117
super(message);
1218
this.data = scrubber.scrub(this.data, true);
1319
}
@@ -21,6 +27,25 @@ export class ApplicationError extends Error {
2127
}
2228
}
2329

30+
export class RepositoryNotFoundError extends ApplicationError {
31+
constructor(readonly info: PlainMessage<RepositoryNotFoundErrorData>) {
32+
// on gRPC we remap to PRECONDITION_FAILED, all error code for backwards compatibility with the dashboard
33+
super(ErrorCodes.NOT_FOUND, "Repository not found.", info);
34+
}
35+
}
36+
export class UnauthorizedRepositoryAccessError extends ApplicationError {
37+
constructor(readonly info: PlainMessage<RepositoryUnauthorizedErrorData>) {
38+
// on gRPC we remap to PRECONDITION_FAILED, all error code for backwards compatibility with the dashboard
39+
super(ErrorCodes.NOT_AUTHENTICATED, "Repository unauthorized.", info);
40+
}
41+
}
42+
export class InvalidGitpodYMLError extends ApplicationError {
43+
constructor(readonly info: PlainMessage<InvalidGitpodYMLErrorData>) {
44+
// on gRPC we remap to PRECONDITION_FAILED, all error code for backwards compatibility with the dashboard
45+
super(ErrorCodes.INVALID_GITPOD_YML, "Invalid gitpod.yml: " + info.violations.join(","), info);
46+
}
47+
}
48+
2449
export namespace ApplicationError {
2550
export function hasErrorCode(e: any): e is Error & { code: ErrorCode; data?: any } {
2651
return ErrorCode.is(e["code"]);

components/gitpod-protocol/src/public-api-converter.spec.ts

Lines changed: 349 additions & 1 deletion
Large diffs are not rendered by default.

components/gitpod-protocol/src/public-api-converter.ts

Lines changed: 234 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,21 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { Timestamp } from "@bufbuild/protobuf";
7+
import { Timestamp, toPlainMessage } from "@bufbuild/protobuf";
88
import { Code, ConnectError } from "@connectrpc/connect";
9+
import {
10+
FailedPreconditionDetails,
11+
ImageBuildLogsNotYetAvailableError,
12+
InvalidCostCenterError as InvalidCostCenterErrorData,
13+
InvalidGitpodYMLError as InvalidGitpodYMLErrorData,
14+
NeedsVerificationError,
15+
PaymentSpendingLimitReachedError,
16+
PermissionDeniedDetails,
17+
RepositoryNotFoundError as RepositoryNotFoundErrorData,
18+
RepositoryUnauthorizedError as RepositoryUnauthorizedErrorData,
19+
TooManyRunningWorkspacesError,
20+
UserBlockedError,
21+
} from "@gitpod/public-api/lib/gitpod/v1/error_pb";
922
import {
1023
AuthProvider,
1124
AuthProviderDescription,
@@ -50,7 +63,13 @@ import {
5063
PrebuildPhase,
5164
PrebuildPhase_Phase,
5265
} from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";
53-
import { ApplicationError, ErrorCode, ErrorCodes } from "./messaging/error";
66+
import {
67+
ApplicationError,
68+
ErrorCodes,
69+
InvalidGitpodYMLError,
70+
RepositoryNotFoundError,
71+
UnauthorizedRepositoryAccessError,
72+
} from "./messaging/error";
5473
import {
5574
AuthProviderEntry as AuthProviderProtocol,
5675
AuthProviderInfo,
@@ -74,7 +93,6 @@ import {
7493
Project,
7594
Organization as ProtocolOrganization,
7695
} from "./teams-projects-protocol";
77-
import { TrustedValue } from "./util/scrubbing";
7896
import {
7997
ConfigurationIdeConfig,
8098
PortProtocol,
@@ -84,9 +102,6 @@ import {
84102
} from "./workspace-instance";
85103
import { Author, Commit } from "@gitpod/public-api/lib/gitpod/v1/scm_pb";
86104

87-
const applicationErrorCode = "application-error-code";
88-
const applicationErrorData = "application-error-data";
89-
90105
/**
91106
* Converter between gRPC and JSON-RPC types.
92107
*
@@ -191,57 +206,240 @@ export class PublicAPIConverter {
191206
return reason;
192207
}
193208
if (reason instanceof ApplicationError) {
194-
const metadata: HeadersInit = {};
195-
metadata[applicationErrorCode] = String(reason.code);
196-
if (reason.data) {
197-
metadata[applicationErrorData] = JSON.stringify(reason.data);
209+
if (reason.code === ErrorCodes.USER_BLOCKED) {
210+
return new ConnectError(
211+
reason.message,
212+
Code.PermissionDenied,
213+
undefined,
214+
[
215+
new PermissionDeniedDetails({
216+
reason: {
217+
case: "userBlocked",
218+
value: new UserBlockedError(),
219+
},
220+
}),
221+
],
222+
reason,
223+
);
224+
}
225+
if (reason.code === ErrorCodes.NEEDS_VERIFICATION) {
226+
return new ConnectError(
227+
reason.message,
228+
Code.PermissionDenied,
229+
undefined,
230+
[
231+
new PermissionDeniedDetails({
232+
reason: {
233+
case: "needsVerification",
234+
value: new NeedsVerificationError(),
235+
},
236+
}),
237+
],
238+
reason,
239+
);
240+
}
241+
if (reason instanceof InvalidGitpodYMLError) {
242+
return new ConnectError(
243+
reason.message,
244+
Code.FailedPrecondition,
245+
undefined,
246+
[
247+
new FailedPreconditionDetails({
248+
reason: {
249+
case: "invalidGitpodYml",
250+
value: new InvalidGitpodYMLErrorData(reason.info),
251+
},
252+
}),
253+
],
254+
reason,
255+
);
256+
}
257+
if (reason instanceof RepositoryNotFoundError) {
258+
return new ConnectError(
259+
reason.message,
260+
Code.FailedPrecondition,
261+
undefined,
262+
[
263+
new FailedPreconditionDetails({
264+
reason: {
265+
case: "repositoryNotFound",
266+
value: new RepositoryNotFoundErrorData(reason.info),
267+
},
268+
}),
269+
],
270+
reason,
271+
);
272+
}
273+
if (reason instanceof UnauthorizedRepositoryAccessError) {
274+
return new ConnectError(
275+
reason.message,
276+
Code.FailedPrecondition,
277+
undefined,
278+
[
279+
new FailedPreconditionDetails({
280+
reason: {
281+
case: "repositoryUnauthorized",
282+
value: new RepositoryUnauthorizedErrorData(reason.info),
283+
},
284+
}),
285+
],
286+
reason,
287+
);
288+
}
289+
if (reason.code === ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED) {
290+
return new ConnectError(
291+
reason.message,
292+
Code.FailedPrecondition,
293+
undefined,
294+
[
295+
new FailedPreconditionDetails({
296+
reason: {
297+
case: "paymentSpendingLimitReached",
298+
value: new PaymentSpendingLimitReachedError(),
299+
},
300+
}),
301+
],
302+
reason,
303+
);
304+
}
305+
if (reason.code === ErrorCodes.INVALID_COST_CENTER) {
306+
return new ConnectError(
307+
reason.message,
308+
Code.FailedPrecondition,
309+
undefined,
310+
[
311+
new FailedPreconditionDetails({
312+
reason: {
313+
case: "invalidCostCenter",
314+
value: new InvalidCostCenterErrorData({
315+
attributionId: reason.data.attributionId,
316+
}),
317+
},
318+
}),
319+
],
320+
reason,
321+
);
322+
}
323+
if (reason.code === ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE) {
324+
return new ConnectError(
325+
reason.message,
326+
Code.FailedPrecondition,
327+
undefined,
328+
[
329+
new FailedPreconditionDetails({
330+
reason: {
331+
case: "imageBuildLogsNotYetAvailable",
332+
value: new ImageBuildLogsNotYetAvailableError(),
333+
},
334+
}),
335+
],
336+
reason,
337+
);
338+
}
339+
if (reason.code === ErrorCodes.TOO_MANY_RUNNING_WORKSPACES) {
340+
return new ConnectError(
341+
reason.message,
342+
Code.FailedPrecondition,
343+
undefined,
344+
[
345+
new FailedPreconditionDetails({
346+
reason: {
347+
case: "tooManyRunningWorkspaces",
348+
value: new TooManyRunningWorkspacesError(),
349+
},
350+
}),
351+
],
352+
reason,
353+
);
198354
}
199355
if (reason.code === ErrorCodes.NOT_FOUND) {
200-
return new ConnectError(reason.message, Code.NotFound, metadata, undefined, reason);
356+
return new ConnectError(reason.message, Code.NotFound, undefined, undefined, reason);
201357
}
202358
if (reason.code === ErrorCodes.NOT_AUTHENTICATED) {
203-
return new ConnectError(reason.message, Code.Unauthenticated, metadata, undefined, reason);
359+
return new ConnectError(reason.message, Code.Unauthenticated, undefined, undefined, reason);
204360
}
205-
if (reason.code === ErrorCodes.PERMISSION_DENIED || reason.code === ErrorCodes.USER_BLOCKED) {
206-
return new ConnectError(reason.message, Code.PermissionDenied, metadata, undefined, reason);
361+
if (reason.code === ErrorCodes.PERMISSION_DENIED) {
362+
return new ConnectError(reason.message, Code.PermissionDenied, undefined, undefined, reason);
207363
}
208364
if (reason.code === ErrorCodes.CONFLICT) {
209-
return new ConnectError(reason.message, Code.AlreadyExists, metadata, undefined, reason);
365+
return new ConnectError(reason.message, Code.AlreadyExists, undefined, undefined, reason);
210366
}
211367
if (reason.code === ErrorCodes.PRECONDITION_FAILED) {
212-
return new ConnectError(reason.message, Code.FailedPrecondition, metadata, undefined, reason);
368+
return new ConnectError(reason.message, Code.FailedPrecondition, undefined, undefined, reason);
213369
}
214370
if (reason.code === ErrorCodes.TOO_MANY_REQUESTS) {
215-
return new ConnectError(reason.message, Code.ResourceExhausted, metadata, undefined, reason);
216-
}
217-
if (reason.code === ErrorCodes.INTERNAL_SERVER_ERROR) {
218-
return new ConnectError(reason.message, Code.Internal, metadata, undefined, reason);
371+
return new ConnectError(reason.message, Code.ResourceExhausted, undefined, undefined, reason);
219372
}
220373
if (reason.code === ErrorCodes.CANCELLED) {
221-
return new ConnectError(reason.message, Code.DeadlineExceeded, metadata, undefined, reason);
374+
return new ConnectError(reason.message, Code.Canceled, undefined, undefined, reason);
222375
}
223-
return new ConnectError(reason.message, Code.InvalidArgument, metadata, undefined, reason);
376+
if (reason.code === ErrorCodes.INTERNAL_SERVER_ERROR) {
377+
return new ConnectError(reason.message, Code.Internal, undefined, undefined, reason);
378+
}
379+
return new ConnectError(reason.message, Code.Unknown, undefined, undefined, reason);
224380
}
225381
return ConnectError.from(reason, Code.Internal);
226382
}
227383

228-
fromError(reason: ConnectError): Error {
229-
const codeMetadata = reason.metadata?.get(applicationErrorCode);
230-
if (!codeMetadata) {
231-
return reason;
384+
fromError(reason: ConnectError): ApplicationError {
385+
if (reason.code === Code.NotFound) {
386+
return new ApplicationError(ErrorCodes.NOT_FOUND, reason.rawMessage);
232387
}
233-
const code = Number(codeMetadata) as ErrorCode;
234-
const dataMetadata = reason.metadata?.get(applicationErrorData);
235-
let data = undefined;
236-
if (dataMetadata) {
237-
try {
238-
data = JSON.parse(dataMetadata);
239-
} catch (e) {
240-
console.error("failed to parse application error data", e);
388+
if (reason.code === Code.Unauthenticated) {
389+
return new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, reason.rawMessage);
390+
}
391+
if (reason.code === Code.PermissionDenied) {
392+
const details = reason.findDetails(PermissionDeniedDetails)[0];
393+
switch (details?.reason?.case) {
394+
case "userBlocked":
395+
return new ApplicationError(ErrorCodes.USER_BLOCKED, reason.rawMessage);
396+
case "needsVerification":
397+
return new ApplicationError(ErrorCodes.NEEDS_VERIFICATION, reason.rawMessage);
398+
}
399+
return new ApplicationError(ErrorCodes.PERMISSION_DENIED, reason.rawMessage);
400+
}
401+
if (reason.code === Code.AlreadyExists) {
402+
return new ApplicationError(ErrorCodes.CONFLICT, reason.rawMessage);
403+
}
404+
if (reason.code === Code.FailedPrecondition) {
405+
const details = reason.findDetails(FailedPreconditionDetails)[0];
406+
switch (details?.reason?.case) {
407+
case "invalidGitpodYml":
408+
const invalidGitpodYmlInfo = toPlainMessage(details.reason.value);
409+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
410+
return new InvalidGitpodYMLError(invalidGitpodYmlInfo);
411+
case "repositoryNotFound":
412+
const repositoryNotFoundInfo = toPlainMessage(details.reason.value);
413+
return new RepositoryNotFoundError(repositoryNotFoundInfo);
414+
case "repositoryUnauthorized":
415+
const repositoryUnauthorizedInfo = toPlainMessage(details.reason.value);
416+
return new UnauthorizedRepositoryAccessError(repositoryUnauthorizedInfo);
417+
case "paymentSpendingLimitReached":
418+
return new ApplicationError(ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED, reason.rawMessage);
419+
case "invalidCostCenter":
420+
const invalidCostCenterInfo = toPlainMessage(details.reason.value);
421+
return new ApplicationError(
422+
ErrorCodes.INVALID_COST_CENTER,
423+
reason.rawMessage,
424+
invalidCostCenterInfo,
425+
);
426+
case "imageBuildLogsNotYetAvailable":
427+
return new ApplicationError(ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE, reason.rawMessage);
428+
case "tooManyRunningWorkspaces":
429+
return new ApplicationError(ErrorCodes.TOO_MANY_RUNNING_WORKSPACES, reason.rawMessage);
241430
}
431+
return new ApplicationError(ErrorCodes.PRECONDITION_FAILED, reason.rawMessage);
432+
}
433+
if (reason.code === Code.ResourceExhausted) {
434+
return new ApplicationError(ErrorCodes.TOO_MANY_REQUESTS, reason.rawMessage);
435+
}
436+
if (reason.code === Code.Canceled) {
437+
return new ApplicationError(ErrorCodes.CANCELLED, reason.rawMessage);
438+
}
439+
if (reason.code === Code.Internal) {
440+
return new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, reason.rawMessage);
242441
}
243-
// data is trusted here, since it was scrubbed before on the server
244-
return new ApplicationError(code, reason.message, new TrustedValue(data));
442+
return new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, reason.rawMessage);
245443
}
246444

247445
toWorkspaceEnvironmentVariables(context: WorkspaceContext): WorkspaceEnvironmentVariable[] {

0 commit comments

Comments
 (0)