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

feat: add error handler to rpc interface #965

Merged
merged 8 commits into from
Nov 28, 2023
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
18 changes: 10 additions & 8 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
- [Highlights](#highlights)
- [Auto-Batching / N+1 Prevention](#auto-batching--n1-prevention)
- [Usage](#usage)
- [Supported options](#supported-options)
- [NestJS Support](#nestjs-support)
- [Watch Mode](#watch-mode)
- [Basic gRPC implementation](#basic-grpc-implementation)
- [Supported options](#supported-options)
- [NestJS Support](#nestjs-support)
- [Watch Mode](#watch-mode)
- [Basic gRPC implementation](#basic-grpc-implementation)
- [Sponsors](#sponsors)
- [Development](#development)
- [Assumptions](#assumptions)
Expand Down Expand Up @@ -121,8 +121,8 @@ plugins:

If you're using a modern TS setup with either `esModuleInterop` or running in an ESM environment, you'll need to pass `ts_proto_opt`s of:

* `esModuleInterop=true` if using `esModuleInterop` in your `tsconfig.json`, and
* `importSuffix=.js` if executing the generated ts-proto code in an ESM environment
- `esModuleInterop=true` if using `esModuleInterop` in your `tsconfig.json`, and
- `importSuffix=.js` if executing the generated ts-proto code in an ESM environment

# Goals

Expand Down Expand Up @@ -446,9 +446,11 @@ Generated code will be placed in the Gradle build directory.

- With `--ts_proto_opt=outputServices=false`, or `=none`, ts-proto will output NO service definitions.

- With `--ts_proto_opt=outputBeforeRequest=true`, ts-proto will add a function definition to the Rpc interface definition with the signature: `beforeRequest(request: <RequestType>)`. It will will also automatically set `outputTypeRegistry=true` and `outputServices=true`. Each of the Service's methods will call `beforeRequest` before performing it's request.
- With `--ts_proto_opt=rpcBeforeRequest=true`, ts-proto will add a function definition to the Rpc interface definition with the signature: `beforeRequest(service: string, message: string, request: <RequestType>)`. It will will also automatically set `outputServices=default`. Each of the Service's methods will call `beforeRequest` before performing it's request.

- With `--ts_proto_opt=outputAfterResponse=true`, ts-proto will add a function definition to the Rpc interface definition with the signature: `afterResponse(response: <ResponseType>)`. It will will also automatically set `outputTypeRegistry=true` and `outputServices=true`. Each of the Service's methods will call `afterResponse` before returning the response.
- With `--ts_proto_opt=rpcAfterResponse=true`, ts-proto will add a function definition to the Rpc interface definition with the signature: `afterResponse(service: string, message: string, response: <ResponseType>)`. It will will also automatically set `outputServices=default`. Each of the Service's methods will call `afterResponse` before returning the response.

- With `--ts_proto_opt=rpcErrorHandler=true`, ts-proto will add a function definition to the Rpc interface definition with the signature: `handleError(service: string, message: string, error: Error)`. It will will also automatically set `outputServices=default`.

- With `--ts_proto_opt=useAbortSignal=true`, the generated services will accept an `AbortSignal` to cancel RPC calls.

Expand Down
12 changes: 8 additions & 4 deletions integration/before-after-request/before-after-request-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { FooServiceClientImpl, FooServiceCreateRequest, FooServiceCreateResponse } from "./simple";
import { MessageType } from "./typeRegistry";
import {
FooServiceClientImpl,
FooServiceCreateRequest,
FooServiceCreateResponse,
FooServiceServiceName,
} from "./simple";

interface Rpc {
request(service: string, method: string, data: Uint8Array): Promise<Uint8Array>;
Expand Down Expand Up @@ -27,14 +31,14 @@ describe("before-after-request", () => {
const req = FooServiceCreateRequest.create(exampleData);
client = new FooServiceClientImpl({ ...rpc, beforeRequest: beforeRequest });
await client.Create(req);
expect(beforeRequest).toHaveBeenCalledWith(req);
expect(beforeRequest).toHaveBeenCalledWith(FooServiceServiceName, "Create", req);
});

it("performs function after request if specified", async () => {
const req = FooServiceCreateRequest.create(exampleData);
client = new FooServiceClientImpl({ ...rpc, afterResponse: afterResponse });
await client.Create(req);
expect(afterResponse).toHaveBeenCalledWith(exampleData);
expect(afterResponse).toHaveBeenCalledWith(FooServiceServiceName, "Create", exampleData);
});

it("doesn't perform function before or after request if they are not specified", async () => {
Expand Down
2 changes: 1 addition & 1 deletion integration/before-after-request/parameters.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
outputBeforeRequest=true,outputAfterResponse=true
rpcBeforeRequest=true,rpcAfterResponse=true,outputServices=default,outputServices=generic-definitions,
24 changes: 20 additions & 4 deletions integration/before-after-request/simple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,23 +432,39 @@ export class FooServiceClientImpl implements FooService {
Create(request: FooServiceCreateRequest): Promise<FooServiceCreateResponse> {
const data = FooServiceCreateRequest.encode(request).finish();
if (this.rpc.beforeRequest) {
this.rpc.beforeRequest(request);
this.rpc.beforeRequest(this.service, "Create", request);
}
const promise = this.rpc.request(this.service, "Create", data);
return promise.then((data) => {
const response = FooServiceCreateResponse.decode(_m0.Reader.create(data));
if (this.rpc.afterResponse) {
this.rpc.afterResponse(response);
this.rpc.afterResponse(this.service, "Create", response);
}
return response;
});
}
}

export type FooServiceDefinition = typeof FooServiceDefinition;
export const FooServiceDefinition = {
name: "FooService",
fullName: "simple.FooService",
methods: {
create: {
name: "Create",
requestType: FooServiceCreateRequest,
requestStream: false,
responseType: FooServiceCreateResponse,
responseStream: false,
options: {},
},
},
} as const;

interface Rpc {
request(service: string, method: string, data: Uint8Array): Promise<Uint8Array>;
beforeRequest?<T extends { [k in keyof T]: unknown }>(request: T): void;
afterResponse?<T extends { [k in keyof T]: unknown }>(response: T): void;
beforeRequest?<T extends { [k in keyof T]: unknown }>(service: string, method: string, request: T): void;
afterResponse?<T extends { [k in keyof T]: unknown }>(service: string, method: string, response: T): void;
}

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
Expand Down
65 changes: 65 additions & 0 deletions integration/handle-error-in-default-service/handle-error-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { GetBasicResponse, GetBasicRequest, BasicServiceClientImpl, BasicServiceServiceName } from "./simple";

interface Rpc {
request(service: string, method: string, data: Uint8Array): Promise<Uint8Array>;
handleError?(service: string, method: string, error: Error): Error;
}

describe("before-after-request", () => {
const exampleData = {
name: "test-name",
};
let rpc = {
request: jest.fn(() => Promise.resolve(new Uint8Array())),
};
let client = new BasicServiceClientImpl(rpc);
let err = new Error("error");

let modifiedError = new Error("modified error");
const handleError = jest.fn(() => modifiedError);

beforeEach(() => {
jest.clearAllMocks();
});

it("doesn't perform handleError if error occurs during encode step", async () => {
const encodeSpy = jest.spyOn(GetBasicRequest, "encode").mockImplementation(() => {
throw err;
});
const req = GetBasicRequest.create(exampleData);
client = new BasicServiceClientImpl({ ...rpc, handleError: handleError });
try {
await client.GetBasic(req);
} catch (error) {
expect(error).toBe(err);
expect(handleError).not.toHaveBeenCalled();
}
encodeSpy.mockRestore();
});

it("performs handleError if error occurs when decoding", async () => {
const decodeSpy = jest.spyOn(GetBasicResponse, "decode").mockImplementation(() => {
throw err;
});
const req = GetBasicRequest.create(exampleData);
client = new BasicServiceClientImpl({ ...rpc, handleError: handleError });
try {
await client.GetBasic(req);
} catch (error) {
expect(error).toBe(modifiedError);
expect(handleError).toHaveBeenCalledWith(BasicServiceServiceName, "GetBasic", err);
}
decodeSpy.mockRestore();
});

it("doesn't perform handleError if it is not specified", async () => {
const req = GetBasicRequest.create(exampleData);
client = new BasicServiceClientImpl(rpc);
try {
await client.GetBasic(req);
} catch (error) {
expect(error).toBe(err);
expect(handleError).not.toHaveBeenCalled();
}
});
});
1 change: 1 addition & 0 deletions integration/handle-error-in-default-service/parameters.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
outputServices=default,rpcErrorHandler=true
Binary file not shown.
14 changes: 14 additions & 0 deletions integration/handle-error-in-default-service/simple.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
syntax = "proto3";
package basic;

message GetBasicRequest {
string name = 1;
}

message GetBasicResponse {
string name = 1;
}

service BasicService {
rpc GetBasic (GetBasicRequest) returns (GetBasicResponse) {}
}
178 changes: 178 additions & 0 deletions integration/handle-error-in-default-service/simple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/* eslint-disable */
import * as _m0 from "protobufjs/minimal";

export const protobufPackage = "basic";

export interface GetBasicRequest {
name: string;
}

export interface GetBasicResponse {
name: string;
}

function createBaseGetBasicRequest(): GetBasicRequest {
return { name: "" };
}

export const GetBasicRequest = {
encode(message: GetBasicRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.name !== "") {
writer.uint32(10).string(message.name);
}
return writer;
},

decode(input: _m0.Reader | Uint8Array, length?: number): GetBasicRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseGetBasicRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}

message.name = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},

fromJSON(object: any): GetBasicRequest {
return { name: isSet(object.name) ? globalThis.String(object.name) : "" };
},

toJSON(message: GetBasicRequest): unknown {
const obj: any = {};
if (message.name !== "") {
obj.name = message.name;
}
return obj;
},

create<I extends Exact<DeepPartial<GetBasicRequest>, I>>(base?: I): GetBasicRequest {
return GetBasicRequest.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<GetBasicRequest>, I>>(object: I): GetBasicRequest {
const message = createBaseGetBasicRequest();
message.name = object.name ?? "";
return message;
},
};

function createBaseGetBasicResponse(): GetBasicResponse {
return { name: "" };
}

export const GetBasicResponse = {
encode(message: GetBasicResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.name !== "") {
writer.uint32(10).string(message.name);
}
return writer;
},

decode(input: _m0.Reader | Uint8Array, length?: number): GetBasicResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseGetBasicResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}

message.name = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},

fromJSON(object: any): GetBasicResponse {
return { name: isSet(object.name) ? globalThis.String(object.name) : "" };
},

toJSON(message: GetBasicResponse): unknown {
const obj: any = {};
if (message.name !== "") {
obj.name = message.name;
}
return obj;
},

create<I extends Exact<DeepPartial<GetBasicResponse>, I>>(base?: I): GetBasicResponse {
return GetBasicResponse.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<GetBasicResponse>, I>>(object: I): GetBasicResponse {
const message = createBaseGetBasicResponse();
message.name = object.name ?? "";
return message;
},
};

export interface BasicService {
GetBasic(request: GetBasicRequest): Promise<GetBasicResponse>;
}

export const BasicServiceServiceName = "basic.BasicService";
export class BasicServiceClientImpl implements BasicService {
private readonly rpc: Rpc;
private readonly service: string;
constructor(rpc: Rpc, opts?: { service?: string }) {
this.service = opts?.service || BasicServiceServiceName;
this.rpc = rpc;
this.GetBasic = this.GetBasic.bind(this);
}
GetBasic(request: GetBasicRequest): Promise<GetBasicResponse> {
const data = GetBasicRequest.encode(request).finish();
const promise = this.rpc.request(this.service, "GetBasic", data);
return promise.then((data) => {
try {
return GetBasicResponse.decode(_m0.Reader.create(data));
} catch (error) {
return Promise.reject(error);
}
}).catch((error) => {
if (error instanceof Error && this.rpc.handleError) {
return Promise.reject(this.rpc.handleError(this.service, "GetBasic", error));
}
return Promise.reject(error);
});
}
}

interface Rpc {
request(service: string, method: string, data: Uint8Array): Promise<Uint8Array>;
handleError?(service: string, method: string, error: Error): Error;
}

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin ? T
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
Loading
Loading