Skip to content

Commit 58c4985

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

File tree

11 files changed

+314
-191
lines changed

11 files changed

+314
-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.ts

Lines changed: 235 additions & 37 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);
198-
}
199209
if (reason.code === ErrorCodes.NOT_FOUND) {
200-
return new ConnectError(reason.message, Code.NotFound, metadata, undefined, reason);
210+
return new ConnectError(reason.message, Code.NotFound, undefined, undefined, reason);
201211
}
202212
if (reason.code === ErrorCodes.NOT_AUTHENTICATED) {
203-
return new ConnectError(reason.message, Code.Unauthenticated, metadata, undefined, reason);
213+
return new ConnectError(reason.message, Code.Unauthenticated, undefined, undefined, reason);
214+
}
215+
if (reason.code === ErrorCodes.USER_BLOCKED) {
216+
return new ConnectError(
217+
reason.message,
218+
Code.PermissionDenied,
219+
undefined,
220+
[
221+
new PermissionDeniedDetails({
222+
reason: {
223+
case: "userBlocked",
224+
value: new UserBlockedError(),
225+
},
226+
}),
227+
],
228+
reason,
229+
);
230+
}
231+
if (reason.code === ErrorCodes.NEEDS_VERIFICATION) {
232+
return new ConnectError(
233+
reason.message,
234+
Code.PermissionDenied,
235+
undefined,
236+
[
237+
new PermissionDeniedDetails({
238+
reason: {
239+
case: "needsVerification",
240+
value: new NeedsVerificationError(),
241+
},
242+
}),
243+
],
244+
reason,
245+
);
204246
}
205-
if (reason.code === ErrorCodes.PERMISSION_DENIED || reason.code === ErrorCodes.USER_BLOCKED) {
206-
return new ConnectError(reason.message, Code.PermissionDenied, metadata, undefined, reason);
247+
if (reason.code === ErrorCodes.PERMISSION_DENIED) {
248+
return new ConnectError(reason.message, Code.PermissionDenied, undefined, undefined, reason);
207249
}
208250
if (reason.code === ErrorCodes.CONFLICT) {
209-
return new ConnectError(reason.message, Code.AlreadyExists, metadata, undefined, reason);
251+
return new ConnectError(reason.message, Code.AlreadyExists, undefined, undefined, reason);
252+
}
253+
if (reason instanceof InvalidGitpodYMLError) {
254+
return new ConnectError(
255+
reason.message,
256+
Code.FailedPrecondition,
257+
undefined,
258+
[
259+
new FailedPreconditionDetails({
260+
reason: {
261+
case: "invalidGitpodYml",
262+
value: new InvalidGitpodYMLErrorData(reason.info),
263+
},
264+
}),
265+
],
266+
reason,
267+
);
268+
}
269+
if (reason instanceof RepositoryNotFoundError) {
270+
return new ConnectError(
271+
reason.message,
272+
Code.FailedPrecondition,
273+
undefined,
274+
[
275+
new FailedPreconditionDetails({
276+
reason: {
277+
case: "repositoryNotFound",
278+
value: new RepositoryNotFoundErrorData(reason.info),
279+
},
280+
}),
281+
],
282+
reason,
283+
);
284+
}
285+
if (reason instanceof UnauthorizedRepositoryAccessError) {
286+
return new ConnectError(
287+
reason.message,
288+
Code.FailedPrecondition,
289+
undefined,
290+
[
291+
new FailedPreconditionDetails({
292+
reason: {
293+
case: "repositoryUnauthorized",
294+
value: new RepositoryUnauthorizedErrorData(reason.info),
295+
},
296+
}),
297+
],
298+
reason,
299+
);
300+
}
301+
if (reason.code === ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED) {
302+
return new ConnectError(
303+
reason.message,
304+
Code.FailedPrecondition,
305+
undefined,
306+
[
307+
new FailedPreconditionDetails({
308+
reason: {
309+
case: "paymentSpendingLimitReached",
310+
value: new PaymentSpendingLimitReachedError(),
311+
},
312+
}),
313+
],
314+
reason,
315+
);
316+
}
317+
if (reason.code === ErrorCodes.INVALID_COST_CENTER) {
318+
return new ConnectError(
319+
reason.message,
320+
Code.FailedPrecondition,
321+
undefined,
322+
[
323+
new FailedPreconditionDetails({
324+
reason: {
325+
case: "invalidCostCenter",
326+
value: new InvalidCostCenterErrorData({
327+
attributionId: reason.data.attributionId,
328+
}),
329+
},
330+
}),
331+
],
332+
reason,
333+
);
334+
}
335+
if (reason.code === ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE) {
336+
return new ConnectError(
337+
reason.message,
338+
Code.FailedPrecondition,
339+
undefined,
340+
[
341+
new FailedPreconditionDetails({
342+
reason: {
343+
case: "imageBuildLogsNotYetAvailable",
344+
value: new ImageBuildLogsNotYetAvailableError(),
345+
},
346+
}),
347+
],
348+
reason,
349+
);
350+
}
351+
if (reason.code === ErrorCodes.TOO_MANY_RUNNING_WORKSPACES) {
352+
return new ConnectError(
353+
reason.message,
354+
Code.FailedPrecondition,
355+
undefined,
356+
[
357+
new FailedPreconditionDetails({
358+
reason: {
359+
case: "tooManyRunningWorkspaces",
360+
value: new TooManyRunningWorkspacesError(),
361+
},
362+
}),
363+
],
364+
reason,
365+
);
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

228384
fromError(reason: ConnectError): Error {
229-
const codeMetadata = reason.metadata?.get(applicationErrorCode);
230-
if (!codeMetadata) {
231-
return reason;
232-
}
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);
385+
if (reason.code === Code.NotFound) {
386+
return new ApplicationError(ErrorCodes.NOT_FOUND, reason.rawMessage);
387+
}
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)