Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap gRPC calls in createClient to allow passing the errors over the context bridge #39230

Merged
merged 12 commits into from Mar 18, 2024

Conversation

gzdunek
Copy link
Contributor

@gzdunek gzdunek commented Mar 12, 2024

Part 2/2 of https://github.com/gravitational/teleport.e/issues/853

The main task of this PR is conversion from grpc-js client to protobuf-ts and then wrapping the calls, so their errors can be inspected.
Changing the client allowed us to remove a lot of the boilerplate in createClient.ts, we also no longer need mapUsageEvent.ts and createFileTransferStream.ts.

@gzdunek gzdunek added teleport-connect Issues related to Teleport Connect. no-changelog Indicates that a PR does not require a changelog entry labels Mar 12, 2024
@gzdunek gzdunek requested review from avatus and removed request for rudream March 12, 2024 12:06
@@ -8,3 +8,4 @@ docs/theme/js/theme.js linguist-vendored=false
*_pb.d.ts linguist-generated
*_pb.ts linguist-generated
*_pb.grpc-*.ts linguist-generated
*_pb.client.ts linguist-generated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, you'll have to update the bot as well, see gravitational/shared-workflows#220.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a reminder about updating the bot.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I will open a PR soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ravicious ravicious self-requested a review March 12, 2024 14:31
Copy link
Member

@ravicious ravicious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks good! I'd like to test the app though, so I'll continue the review tomorrow.


// Following cases should never happen but just in case?
case api.PasswordlessPrompt.UNSPECIFIED:
stream.requests.complete();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is complete the same as cancel?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really (but this doesn't seem to make a difference here).
complete: Complete / close the stream. Can only be called if there is no pending send(). No send() should follow this call.
cancel: Cancel the ongoing call. Results in the call ending with a CANCELLED status, unless it has already ended with some other status.

I think this doesn't matter here anyway, because just after calling these functions we do return reject(new Error(..), so any gRPC response is discarded.
However, I feel like this code is (and was) prone to race conditions. There is no guarantee that onMessage/onError handler is called after our rejection.

Ideal solution for this would be to add AbortController inside the handler and call abort for the cases above. Then combine its abortSignal with the abort signal from the callsite with AbortSignal.any.
Thanks to that, all errors would go the same route.
There two problems with this tho:

  • AbortSignal.any is supported since Node.js 20.3 (but we update soon).
  • The abort reason is not propagated, the only error we get is 'Cancelled on client' :(

So I think we have to stay with stream.requests.complete()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see what you mean about AbortSignal.any.

Is "Cancelled on client" generated by the library or by us? We could probably improve the UX a little bit by checking if the error is an abort error when displaying the error in the modal. But that's for another PR I suppose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "Cancelled on client" generated by the library or by us?

It's generated by the library.

We could probably improve the UX a little bit by checking if the error is an abort error when displaying the error in the modal.

Hmm, but what that would give us? I guess, only a different error message?
I think that ideally it could work like this:

const internalAbortController = new AbortController();
const anySignal = Abort.any([internalAbortController.signal, abortSignal]);
const stream = tshd.loginPasswordless({
   abort: anySignal,
});

stream.responses.onError(function (err: Error) {
   let e: Error | TshdRpcError;
   if (isTshdRpcError(err) && err.code === 'CANCELLED') {
      e = {
         ...err,
         message: `${err.message}: ${anySignal.reason}`,
      };
    } else {
        e = err;
    }
    reject(e);
})

But I would need to update electron & node first.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it'd mostly be the error message. "Cancelled on client" sounds much worse than not showing the error at all if we know that the login process was just cancelled by the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can think of it. For now, I will keep it as is, since we have this behavior from the beginning.

web/packages/teleterm/src/services/tshd/createClient.ts Outdated Show resolved Hide resolved
return (error.message as string)?.includes('access denied');
import { isTshdRpcError } from './cloneableClient';

export function isAccessDeniedError(error: unknown): boolean {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably deprecate all functions in this file and make callsites use something like isTshdRpcError(error, 'NOT_FOUND'), where the second argument belongs to a discriminated union.

But this is not a PR which addresses the errors, we're focused on the gRPC client instead, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isTshdRpcError(error, 'NOT_FOUND')

That's a nice idea, I didn't think of it. But in general, I wanted to limit the number of changes and only updated the implementation here.

We can switch to isTshdRpcError(error, 'NOT_FOUND') in a separate PR.

@ravicious ravicious self-requested a review March 14, 2024 14:42
Copy link
Member

@ravicious ravicious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested passwordless login (by cancelling every step), headless auth and access requests. Everything appears to be in order.

(Though, sometimes headless requests stop working and I can't pinpoint why. As if tshd stopped listening to them. idk if it's something you perhaps ran into as well.)

@@ -8,3 +8,4 @@ docs/theme/js/theme.js linguist-vendored=false
*_pb.d.ts linguist-generated
*_pb.ts linguist-generated
*_pb.grpc-*.ts linguist-generated
*_pb.client.ts linguist-generated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a reminder about updating the bot.


// Following cases should never happen but just in case?
case api.PasswordlessPrompt.UNSPECIFIED:
stream.requests.complete();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see what you mean about AbortSignal.any.

Is "Cancelled on client" generated by the library or by us? We could probably improve the UX a little bit by checking if the error is an abort error when displaying the error in the modal. But that's for another PR I suppose.

@gzdunek gzdunek merged commit ed2029c into gzdunek/cloneable-client Mar 18, 2024
35 of 36 checks passed
@gzdunek gzdunek deleted the grzdunek/wrap-grpc-calls branch March 18, 2024 15:57
github-merge-queue bot pushed a commit that referenced this pull request Mar 18, 2024
* Add functions to clone every gRPC method, so they can be passed over the context bridge

* Replace `@grpc/grpc-js` interceptors with `@protobuf-ts/runtime-rpc` ones

* Replace string check with check on metadata, add missing `AddMetadataToRetryableError`

* Add `pin` to sensitive properties

It's needed because now we also log a passwordless login stream call.

* Add `cloneClient` method to clone the entire gRPC client

* Switch tests to `TshdRetryableError`

* Improve comments

* Move `cloneClient` and `cloneAbortSignal` to the top of the file

* Simplify condition

* Lowercase logs

* Check if the error is `RpcError` using `instanceof`

* Log error object instead of `[object Object]`

* Do not export call specific types

* Wrap gRPC calls in `createClient` to allow passing the errors over the context bridge (#39230)

* Regenerate protos with the `protobuf-ts` client

* Enable `strictBindCallApply` so `.bind()` results have correct types (instead of `any`)

This is needed for wrapping calls in `createClient`.

* Use `protobuf-ts` client instead of `grpc-js` one

* Convert `createClient` to use `protobuf-ts` response style, clone each call

* Switch callsites to `cloneAbortSignal`

* Replace `error.message` checks with a proper check on the error status code

* Clone the entire client instead of each method separately

* Correct the error `cause` in `ResourceSearchError`

* Remove `params.sortBy` defaults, always set `startKey` to string

* Use a simpler type for `reportUsageEvent`

* Fix interceptor test

* Ignore TS error in `highbar.ts`
gzdunek added a commit that referenced this pull request Mar 22, 2024
* Add functions to clone every gRPC method, so they can be passed over the context bridge

* Replace `@grpc/grpc-js` interceptors with `@protobuf-ts/runtime-rpc` ones

* Replace string check with check on metadata, add missing `AddMetadataToRetryableError`

* Add `pin` to sensitive properties

It's needed because now we also log a passwordless login stream call.

* Add `cloneClient` method to clone the entire gRPC client

* Switch tests to `TshdRetryableError`

* Improve comments

* Move `cloneClient` and `cloneAbortSignal` to the top of the file

* Simplify condition

* Lowercase logs

* Check if the error is `RpcError` using `instanceof`

* Log error object instead of `[object Object]`

* Do not export call specific types

* Wrap gRPC calls in `createClient` to allow passing the errors over the context bridge (#39230)

* Regenerate protos with the `protobuf-ts` client

* Enable `strictBindCallApply` so `.bind()` results have correct types (instead of `any`)

This is needed for wrapping calls in `createClient`.

* Use `protobuf-ts` client instead of `grpc-js` one

* Convert `createClient` to use `protobuf-ts` response style, clone each call

* Switch callsites to `cloneAbortSignal`

* Replace `error.message` checks with a proper check on the error status code

* Clone the entire client instead of each method separately

* Correct the error `cause` in `ResourceSearchError`

* Remove `params.sortBy` defaults, always set `startKey` to string

* Use a simpler type for `reportUsageEvent`

* Fix interceptor test

* Ignore TS error in `highbar.ts`

(cherry picked from commit 784d7ac)
github-merge-queue bot pushed a commit that referenced this pull request Mar 25, 2024
…ss denied" (#39720)

* Clone the tshd gRPC client to allow inspecting errors (#39229)

* Add functions to clone every gRPC method, so they can be passed over the context bridge

* Replace `@grpc/grpc-js` interceptors with `@protobuf-ts/runtime-rpc` ones

* Replace string check with check on metadata, add missing `AddMetadataToRetryableError`

* Add `pin` to sensitive properties

It's needed because now we also log a passwordless login stream call.

* Add `cloneClient` method to clone the entire gRPC client

* Switch tests to `TshdRetryableError`

* Improve comments

* Move `cloneClient` and `cloneAbortSignal` to the top of the file

* Simplify condition

* Lowercase logs

* Check if the error is `RpcError` using `instanceof`

* Log error object instead of `[object Object]`

* Do not export call specific types

* Wrap gRPC calls in `createClient` to allow passing the errors over the context bridge (#39230)

* Regenerate protos with the `protobuf-ts` client

* Enable `strictBindCallApply` so `.bind()` results have correct types (instead of `any`)

This is needed for wrapping calls in `createClient`.

* Use `protobuf-ts` client instead of `grpc-js` one

* Convert `createClient` to use `protobuf-ts` response style, clone each call

* Switch callsites to `cloneAbortSignal`

* Replace `error.message` checks with a proper check on the error status code

* Clone the entire client instead of each method separately

* Correct the error `cause` in `ResourceSearchError`

* Remove `params.sortBy` defaults, always set `startKey` to string

* Use a simpler type for `reportUsageEvent`

* Fix interceptor test

* Ignore TS error in `highbar.ts`

(cherry picked from commit 784d7ac)

* Do not replace `AccessDenied` error messages with generic "access denied" (#39558)

* Do not replace `AccessDenied` error messages with generic "access denied"

* Replace `error.code` checks with `isTshdRpcError(error, code)`

* Add `ABORTED` and `UNAUTHENTICATED` codes

* Improve `isTshdRpcError` documentation

* Do not catch `retryWithRelogin` errors in `Setup.tsx`

(cherry picked from commit 48c05b6)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-changelog Indicates that a PR does not require a changelog entry size/lg teleport-connect Issues related to Teleport Connect. ui
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants