Skip to content

Commit 5ca0513

Browse files
authored
Proper category errors from serverApi and publicApi IDE-162 (#80)
* Proper category errors from serverApi and publicApi * Align to public-api
1 parent 15de809 commit 5ca0513

File tree

3 files changed

+63
-5
lines changed

3 files changed

+63
-5
lines changed

src/authentication/authentication.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export default class GitpodAuthenticationProvider extends Disposable implements
176176
this.logService.info(`Verified session with the following scopes: ${scopesStr}`);
177177
} catch (e) {
178178
// Remove sessions that return unauthorized response
179-
if (e.message === 'Unexpected server response: 401') {
179+
if (e.message.includes('Unexpected server response: 401')) {
180180
return undefined;
181181
}
182182
this.logService.error(`Error while verifying session with the following scopes: ${scopesStr}`, e);

src/internalApi.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55

66
import { GitpodClient, GitpodServer, GitpodServiceImpl } from '@gitpod/gitpod-protocol/lib/gitpod-service';
77
import { JsonRpcProxyFactory } from '@gitpod/gitpod-protocol/lib/messaging/proxy-factory';
8-
import { listen as doListen } from 'vscode-ws-jsonrpc';
8+
import { listen as doListen, ResponseError } from 'vscode-ws-jsonrpc';
99
import WebSocket, { ErrorEvent } from 'ws';
1010
import ReconnectingWebSocket from 'reconnecting-websocket';
1111
import * as vscode from 'vscode';
1212
import { ILogService } from './services/logService';
13+
import { Code } from '@bufbuild/connect';
14+
import { WrapError } from './common/utils';
1315

1416
type UsedGitpodFunction = ['getLoggedInUser', 'getWorkspace', 'getOwnerToken', 'getSSHPublicKeys', 'sendHeartBeat'];
1517
type Union<Tuple extends any[], Union = never> = Tuple[number] | Union;
@@ -58,6 +60,11 @@ class GitpodServerApi extends vscode.Disposable {
5860
});
5961
webSocket.onerror = (e: ErrorEvent) => {
6062
if (webSocket.retryCount >= maxRetries) {
63+
// https://github.com/gitpod-io/gitpod/blob/d41a38ba83939856e5292e30912f52e749787db1/components/server/src/server.ts#L193-L195
64+
if (e.error.message === 'Unexpected server response: 401') {
65+
this.onErrorEmitter.fire(new WrapError('Failed to call server API', e.error, 'ServerAPI:'+Code[Code.Unauthenticated]));
66+
return;
67+
}
6168
this.onErrorEmitter.fire(e.error);
6269
}
6370
};
@@ -84,9 +91,46 @@ class GitpodServerApi extends vscode.Disposable {
8491
export function withServerApi<T>(accessToken: string, serviceUrl: string, cb: (service: GitpodConnection) => Promise<T>, logger: ILogService): Promise<T> {
8592
const api = new GitpodServerApi(accessToken, serviceUrl, logger);
8693
return Promise.race([
87-
cb(api.service),
94+
new Promise<T>((resolve, reject) => cb(api.service).then(resolve).catch(err => {
95+
if (err instanceof ResponseError) {
96+
const code = categorizeRPCError(err);
97+
const codeStr = code ? Code[code] : 'Unknown';
98+
reject(new WrapError('Failed to call server API', err, 'ServerAPI:' + codeStr));
99+
return;
100+
}
101+
reject(err);
102+
})),
88103
new Promise<T>((_, reject) => api.onError(error => {
89104
reject(error);
90105
}))
91106
]).finally(() => api.dispose());
92107
}
108+
109+
// Should align with https://github.com/gitpod-io/gitpod/blob/d41a38ba83939856e5292e30912f52e749787db1/components/public-api-server/pkg/proxy/errors.go#LL25C1-L26C1
110+
function categorizeRPCError(err?: ResponseError<any>): Code | undefined {
111+
if (!err) {
112+
return;
113+
}
114+
switch (err.code) {
115+
case 400:
116+
return Code.InvalidArgument;
117+
case 401:
118+
return Code.Unauthenticated;
119+
case 403:
120+
return Code.PermissionDenied;
121+
case 404:
122+
return Code.NotFound;
123+
case 409:
124+
return Code.AlreadyExists;
125+
case 429:
126+
return Code.ResourceExhausted;
127+
case 470:
128+
return Code.PermissionDenied;
129+
case -32603:
130+
return Code.Internal;
131+
}
132+
if (err.code >= 400 && err.code < 500) {
133+
return Code.InvalidArgument;
134+
}
135+
return Code.Internal;
136+
}

src/publicApi.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { createConnectTransport } from '@bufbuild/connect-node';
7-
import { createPromiseClient, Interceptor, PromiseClient } from '@bufbuild/connect';
7+
import { createPromiseClient, Interceptor, PromiseClient, ConnectError, Code } from '@bufbuild/connect';
88
import { WorkspacesService } from '@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_connectweb';
99
import { IDEClientService } from '@gitpod/public-api/lib/gitpod/experimental/v1/ide_client_connectweb';
1010
import { UserService } from '@gitpod/public-api/lib/gitpod/experimental/v1/user_connectweb';
@@ -18,6 +18,7 @@ import { getErrorCode } from '@grpc/grpc-js/build/src/error';
1818
import { timeout } from './common/async';
1919
import { MetricsReporter, getConnectMetricsInterceptor, getGrpcMetricsInterceptor } from './metrics';
2020
import { ILogService } from './services/logService';
21+
import { WrapError } from './common/utils';
2122

2223
function isTelemetryEnabled(): boolean {
2324
const TELEMETRY_CONFIG_ID = 'telemetry';
@@ -68,11 +69,24 @@ export class GitpodPublicApi extends Disposable implements IGitpodAPI {
6869
return await next(req);
6970
};
7071
const metricsInterceptor = getConnectMetricsInterceptor();
72+
const errorWrapInterceptor: Interceptor = (next) => async (req) => {
73+
try {
74+
return await next(req);
75+
} catch (err) {
76+
if (err instanceof ConnectError) {
77+
// https://github.com/gitpod-io/gitpod/blob/d41a38ba83939856e5292e30912f52e749787db1/components/public-api-server/pkg/auth/middleware.go#L73
78+
// https://github.com/gitpod-io/gitpod/blob/d41a38ba83939856e5292e30912f52e749787db1/components/public-api-server/pkg/proxy/errors.go#L30
79+
// NOTE: WrapError will omit error's other properties
80+
throw new WrapError('Failed to call public API', err, 'PublicAPI:' + Code[err.code]);
81+
}
82+
throw err;
83+
}
84+
};
7185

7286
const transport = createConnectTransport({
7387
baseUrl: serviceUrl.toString(),
7488
httpVersion: '2',
75-
interceptors: [authInterceptor, metricsInterceptor],
89+
interceptors: [errorWrapInterceptor, authInterceptor, metricsInterceptor],
7690
useBinaryFormat: true,
7791
});
7892

0 commit comments

Comments
 (0)