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

Enhance HTTP Status handling #1472

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

wszydlak
Copy link

@wszydlak wszydlak commented Jan 20, 2025

Pull Request: Enhance HTTP Status handling in unary calls

Background

When using gRPC-Web with a reverse proxy like NGINX, there are scenarios where the proxy may return HTTP status codes (e.g., 503, 505) without invoking the actual gRPC service. In such cases, the gRPC-Web library throws an RpcError with a StatusCode.UNKNOWN. This limits the ability to differentiate between HTTP errors and makes it challenging to implement robust client-side error handling based on specific HTTP statuses.

Changes introduced in this PR

  1. Addition of httpStatusCode to Errors

    • Modified the error creation logic in grpcwebclientreadablestream.js to include the httpStatusCode from the XHR object.
    • This ensures that the exact HTTP status code is accessible in unary call errors.
    • HTTP Status is added as string to fit Metadata type
  2. Enhanced complete event for streaming

    • Updated the behavior of the complete event for streaming calls to mirror the unary call changes.
    • The httpStatusCode is now included in metadata when firing status event.

Alternatives

As an alternative resolution i can prepare solution similar to this line, so the final result for UNKNOWN error could be handled like:

-    new RpcError(StatusCode.UNKNOWN, 'Unknown Content-type received.'));
+    new RpcError(StatusCode.UNKNOWN, 'Unknown Content-type received, http status code: ' + self.xhr_.getStatus()));

Example behaviour before and after this change

Before:

Unary call errors always return RpcError with StatusCode.UNKNOWN, irrespective of the HTTP status code.

After:

Errors now include the httpStatusCode field in metadata, enabling better decision-making on the client side.

Example:

{
  code: StatusCode.UNKNOWN,
  message: "An error occurred",
  metadata: {
    httpStatusCode: '505'
  }
}

Open Questions

  1. Acceptability: Is adding the httpStatusCode to the RpcError object an acceptable enhancement for unary and streaming calls?
  2. Metadata Handling: Should we consider modifying metadata to accept numbers for better handling of HTTP-specific errors?
  3. Alternative: Is proposed solution ok, or you want to try with proposed alternative?

Why This Change Is Valuable

  • Improved Client-Side Error Handling: Enables applications to differentiate between HTTP-specific errors (e.g., 503, 505) and gRPC errors, particularly when intermediary layers like NGINX are used.
  • Consistency: Aligns the behaviour of unary calls with streaming calls, ensuring both provide detailed error context.
  • Enhanced Debugging: Provides developers with the HTTP status code directly, reducing time spent diagnosing issues caused by reverse proxies.

Copy link

linux-foundation-easycla bot commented Jan 20, 2025

CLA Signed

The committers listed above are authorized under a signed CLA.

  • ✅ login: wszydlak / name: Wojtek Szydlak (b84dc9c)

@wszydlak wszydlak changed the title Enhance HTTP Status Handling in Unary Calls for gRPC-Web Enhance HTTP Status handling in unary calls Jan 20, 2025
@wszydlak
Copy link
Author

@sampajano any chance to move this PR forward?

Copy link
Collaborator

@sampajano sampajano left a comment

Choose a reason for hiding this comment

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

@wszydlak Thank you so much for proposing this change with detailed rationales (even with alternatives)!! Appreciate!

I apologize for the late review! — I was out for a few weeks and have been catching up on things :)

Overall i think this change makes a lot of sense!! And i certainly like it better than the alternative solution — since it provides more structured data and can be better accessed programmatically 😃

Regarding your questions:

  • Acceptability: Is adding the httpStatusCode to the RpcError object an acceptable enhancement for unary and streaming calls?

I think this is a fairly reasonable improvement to the API!

  • Metadata Handling: Should we consider modifying metadata to accept numbers for better handling of HTTP-specific errors?

I'm not quite understanding here. Do you mind elaborate a bit (maybe with examples)?

  • Alternative: Is proposed solution ok, or you want to try with proposed alternative?

I like this solution better since the data is more programmatically accessible!


One question tho:

Are you intending to make this "unary only" feature? Why not make this accessible across the board even for streaming then?

From what I could tell, the following maybe the only other missing scenario where we can return an RpcError from xhr errors:

if (xhrStatusCode != -1) {
errorMessage += ', http status code: ' + xhrStatusCode;
}
self.handleError_(new RpcError(grpcStatusCode, errorMessage));

Maybe we could simply add httpStatusCode here as well?


And one more thing, could we add a simple test?

Maybe just add a new test like this one?


Again, thank you so much for proposing this improvement! I think it makes much sense and I appreciate your contribution!

@wszydlak
Copy link
Author

wszydlak commented Feb 1, 2025

Hello, thanks for the response

I appreciate that you see this improvement is reasonable.

Are you intending to make this "unary only" feature? Why not make this accessible across the board even for streaming then?

PR contains also changes for the regular streaming: https://github.com/wszydlak/grpc-web/blob/feature/http-status-on-error/javascript/net/grpc/web/grpcwebclientreadablestream.js#L265

Metadata Handling: Should we consider modifying metadata to accept numbers for better handling of HTTP-specific errors?

I'm not quite understanding here. Do you mind elaborate a bit (maybe with examples)?

Sure, i mean the types in the grpc-web package: https://github.com/wszydlak/grpc-web/blob/feature/http-status-on-error/packages/grpc-web/index.d.ts#L3
Should we keep metadata as Record<string, string> or maybe we could go this way:

export type Metadata = Record<string, string> & { httpStatusCode?: number };

This will allow us to handle httpStatusCode as numbers, not as strings as proposed in current PR.
Current solution converts code to string to be compatible with current Metadata type, but maybe we can make a change here?

And one more thing, could we add a simple test?

Sure, if we will decide what should be the final implementation i will also prepare tests 🙂

@wszydlak wszydlak requested a review from sampajano February 1, 2025 19:17
@wszydlak wszydlak changed the title Enhance HTTP Status handling in unary calls Enhance HTTP Status handling Feb 3, 2025
@sampajano
Copy link
Collaborator

Hello, thanks for the response

I appreciate that you see this improvement is reasonable.

Are you intending to make this "unary only" feature? Why not make this accessible across the board even for streaming then?

PR contains also changes for the regular streaming: https://github.com/wszydlak/grpc-web/blob/feature/http-status-on-error/javascript/net/grpc/web/grpcwebclientreadablestream.js#L265

Oh nice! Wonderful! Thanks for reminding me :)

I saw that you've put "unary" in PR title and title and hence the question. Do you mind updating that too?

Metadata Handling: Should we consider modifying metadata to accept numbers for better handling of HTTP-specific errors?

I'm not quite understanding here. Do you mind elaborate a bit (maybe with examples)?

Sure, i mean the types in the grpc-web package: https://github.com/wszydlak/grpc-web/blob/feature/http-status-on-error/packages/grpc-web/index.d.ts#L3 Should we keep metadata as Record<string, string> or maybe we could go this way:

export type Metadata = Record<string, string> & { httpStatusCode?: number };

This will allow us to handle httpStatusCode as numbers, not as strings as proposed in current PR. Current solution converts code to string to be compatible with current Metadata type, but maybe we can make a change here?

Ahh nice.. I like the idea! Why not! :)

And one more thing, could we add a simple test?

Sure, if we will decide what should be the final implementation i will also prepare tests 🙂

Yes. I'm happy with the solution and will merge it once a test is added!

Thanks again! :)

@wszydlak wszydlak force-pushed the feature/http-status-on-error branch 2 times, most recently from 9ef3774 to 82f7f36 Compare February 5, 2025 13:04
@wszydlak
Copy link
Author

wszydlak commented Feb 5, 2025

Hello again @sampajano, coming back with some changes:

  1. When i tried to run mocha tests they didn't work, cause third_party/protobuf/js did not exist, probably using old version, as protobuf/js was moved in v21 to separate protobuf-javascript repository - so i decided to restore mocha test by adding submodule and changing paths.
  2. Changed Metadata to recognize httpStatusCode as number
  3. Modified a little readystatechange event, not to send RpcError when Content-Type of response is not matching application/grpc* - errors will be handled by complete event which for other Content-Type occurs right after readystatechange. This allowed to handle httpStatusCode in single place in code - it will work for unary and streaming
  4. Added tests to verify such cases of receiving Content-Type not matching application/grpc*

I ran the mocha tests and generated test for javascript/net/grpc/web/grpcwebclientbase_test.js

Please confirm that proposed solution is acceptable and if everything looks good or maybe some changes are required.

Additionally, if everything is ok - can we schedule release date of 1.6.0 (assuming this version will contain proposed changes)?

@wszydlak wszydlak force-pushed the feature/http-status-on-error branch from 82f7f36 to b84dc9c Compare February 6, 2025 06:41
@wszydlak
Copy link
Author

wszydlak commented Feb 6, 2025

@sampajano
I see tests failing due to missing new submodule.
I didn't notice this script: https://github.com/grpc/grpc-web/blob/master/scripts/init_submodules.sh which forces to use older protobuf repository version.

I reverted commit with change of the submodule.

Maybe this should be addressed in separate PR.

Waiting for the tests re-run.

@wszydlak
Copy link
Author

@sampajano I see tests are passing, what are the next steps?

@sampajano
Copy link
Collaborator

@wszydlak Hi! My apologies that I was caught up in something and not able to find time for this just yet, and will be out until mid next week. I'll review this as soon as I'm back and will get it merged soon!

And yes! We're recently getting some new help on grpc-web now so we'll do a release soon!

@wszydlak
Copy link
Author

@wszydlak Hi! My apologies that I was caught up in something and not able to find time for this just yet, and will be out until mid next week. I'll review this as soon as I'm back and will get it merged soon!

And yes! We're recently getting some new help on grpc-web now so we'll do a release soon!

Sure, waiting then for a review 👍 Thanks 🙂

Copy link
Collaborator

@sampajano sampajano left a comment

Choose a reason for hiding this comment

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

Hi! Thanks for the change! I've started to review it and have a first few questions! Thanks!

let contentType = self.xhr_.getStreamingResponseHeader('Content-Type');
if (!contentType) return;
contentType = contentType.toLowerCase();
const contentType = (self.xhr_.getStreamingResponseHeader('Content-Type') || '').toLowerCase();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you explain a bit more on the motivation of all these changes (besides just adding the httpStatusCode in metadata)?

It looks to me that it's altering some existing logic, and I'm not sure if it can cause regressions.

From the PR description, I got the impression that adding httpStatusCode was all we needed.

Is that not the case?

Copy link
Author

Choose a reason for hiding this comment

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

Hi, already described it above: #1472 (comment)

@@ -257,11 +253,14 @@ class GrpcWebClientReadableStream {
return;
}
let errorMessage = ErrorCode.getDebugMessage(lastErrorCode);

const errorMetadata = /** @type {!StatusMetadata} */ ({});
Copy link
Collaborator

Choose a reason for hiding this comment

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

This line won't pass closure compilation (it will break internal tests, but not sure why it's not throwing up in tests :)) — since StatusMetadata is not a defined type in Closure JS.

Copy link
Author

Choose a reason for hiding this comment

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

How then can we define it as a type for Closure JS? How is Metadata defined in closure js?

Copy link
Author

@wszydlak wszydlak Feb 24, 2025

Choose a reason for hiding this comment

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

Is this about creating file like this: javascript/net/grpc/web/metadata.js ?

@@ -1,6 +1,9 @@
declare module "grpc-web" {

export interface Metadata { [s: string]: string; }
export type StatusMetadata = Metadata & {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I appreciate the type extension, and i personally prefer this kind of change.

However, since I plan to have this feature only in open source grpc-web, i prefer to make minimal changes to the rest of the API (e.g. in RpcError, etc.). Would it be possible if you just modify Metadata instead?

Copy link
Author

Choose a reason for hiding this comment

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

That was my first tought, but, Metadata type is used both for input and output to grpc-services. Also i assume that previous definition od Metadata follows this: https://grpc.io/docs/guides/metadata/#overview (keys are string, values are string or binary).

To not make a breaking change for those who rely on Record<string, string> i created separate Metadata for request statuses (status + error) that additionaly can have new httpStatusCode property of number type.

I think this is more secure way to handle Metadata for status and error.

Don't see any reason why httpStatusCode should be valid property for standard messaging (in-out).

That's why i decided to make separate type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants