Skip to content

fix: handle NaN and Infinity float values in gRPC #4631

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

Merged
merged 25 commits into from
Jul 31, 2025

Conversation

ariasmn
Copy link
Contributor

@ariasmn ariasmn commented Mar 15, 2025

Fixes (?): #3990

The problem was in this specific line:

b, err := req.ToObject(c.vu.Runtime()).MarshalJSON()

This MarshalJSON implemented in Sobek uses a copy of JSON.stringify() under the hood, and per JSON RFC, neither Infinity or NaN as values are accepted as valid numbers.

However, since we rely on ProtoJSON, which does support special strings "Infinity" and "NaN", I guess that the idea would be to "normalize" them as a string format before marshaling, which is what this PR implements.

@ariasmn ariasmn force-pushed the fix/grpc-special-string-float branch from 2fa5e78 to fdd70b2 Compare March 15, 2025 11:46
@ariasmn
Copy link
Contributor Author

ariasmn commented Mar 15, 2025

Couple of things:

  • Having a hard time trying to understand how to build the tests (I'm guessing that it should be a test case in here). I just checked that current tests pass and added one for wrapped double value, but I guess I'd have to add another one for a normal float proto.
  • I still don't think that this approach is good enough... I feel like maybe the best bet would be to create the proto message using protoreflect and then marshaling normally, but I haven't tried if that would break wrappers and reflection implementations (and also is a bit long in terms of code, atleast the way I did it at my company).

@ariasmn ariasmn marked this pull request as ready for review March 20, 2025 15:26
@ariasmn ariasmn requested a review from a team as a code owner March 20, 2025 15:26
@ariasmn ariasmn requested review from mstoykov and joanlopez and removed request for a team March 20, 2025 15:26
@joanlopez joanlopez requested a review from oleiade March 20, 2025 15:50
Copy link
Contributor

@mstoykov mstoykov left a comment

Choose a reason for hiding this comment

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

Thank you for the PR and sorry for the slow reply 🙇

I have left some comments around the code.

On the whole idea I am still not particularly certain this is ... what should be done, if anything.

Trying to figure out how this works in other cases I found protocolbuffers/protobuf-javascript#49 which seems to suggest that this isn't really supported. But maybe I am getting something wrong.

From the test it seems that this just works on the way back which seems awfully convient.

@ariasmn
Copy link
Contributor Author

ariasmn commented Apr 11, 2025

Hey @mstoykov, thanks a lot for the review!

I’ve made some changes based on your comments. There’s one part I didn't change yet because I’m not fully sure I understand it correctly, but we can keep discussing it in the open conversation.

Regarding the issue you mentioned: it looks like an older open issue. From what I saw, the code has since changed and now properly asserts both Infinity and NaN:

https://github.com/protocolbuffers/protobuf-javascript/blob/0768cc9f18bec192c393d7c2962ee8ab5ad1931b/binary/encoder.js#L411-L418

I also tested it locally using the dummy.proto from the issue, for both client and server (wire format). Additionally, the ProtoJSON documentation confirms that both values are officially supported.

Just a heads-up: I might be a bit slower next week if any further changes are needed — apologies in advance.

Thanks again for the great feedback. 🙏

EDIT: Tests are failing, don't know if related to my changes but the client_test.go tests are passing correctly locally 🤔

@ariasmn ariasmn requested a review from mstoykov May 2, 2025 18:47
@ariasmn
Copy link
Contributor Author

ariasmn commented Jun 20, 2025

Hi there,

sorry for the delay on this! It's been a while since I could get back to this PR (I haven't had much time recently).

I've made the requested corrections. Please take a look and let me know if everything looks good (I feel like there should be a better way of handling the copy for sure) or if there's anything else that needs to be addressed.

Thank you! 🙇

@mstoykov mstoykov force-pushed the fix/grpc-special-string-float branch from adbc0f9 to 0a673e0 Compare July 17, 2025 13:26
@joanlopez
Copy link
Contributor

joanlopez commented Jul 22, 2025

Hi @ariasmn,

Sorry for the late review, but finally I've been able to properly test it.

I think it's close to be ready to be merged, but there's one extra thing that we should fix, which is that if normalizeNumberStrings receives a cyclic reference to an object (see below), it will end up in an infinite loop of recursive calls that eventually will lead to a panic.

For instance:

client.connect("GRPCBIN_ADDR");

let obj1 = {};
let obj2 = {};
obj1.a = obj2;
obj2.b = obj1;  // Now both objects reference each other

let respNaN = client.invoke("grpc.wrappers.testing.Service/TestDouble", obj1); // <-- this causes the panic

Can you take a look at this and try to fix it, please? Thanks!

@joanlopez joanlopez added this to the v1.2.0 milestone Jul 22, 2025
@joanlopez joanlopez added awaiting user waiting for user to respond area: grpc labels Jul 22, 2025
@joanlopez joanlopez self-assigned this Jul 22, 2025
@ariasmn
Copy link
Contributor Author

ariasmn commented Jul 27, 2025

Hi @joanlopez,

Thank you for catching that one! I implemented a fix by following what's done in the stringify method in Sobek, and added a test case as well:

case *Object:
for _, object := range ctx.stack {
if value1.SameAs(object) {
ctx.r.typeErrorResult(true, "Converting circular structure to JSON")
}
}
ctx.stack = append(ctx.stack, value1)

Let me know if my choice of wording for variables/comments is odd, I can fix them no problem.

Thanks!

}

// Handle special float values.
if f, ok := v.Export().(float64); ok {
Copy link
Contributor

Choose a reason for hiding this comment

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

As @mstoykov suggested before, could we use ExportType() here instead of Export(), as the latter is a much more expensive operation.

In case it's a float64, I guess you'll need to call v.Export() anyway, but in all the other cases, it won't be called that early.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or maybe even something like:

Suggested change
if f, ok := v.Export().(float64); ok {
if v.ExportType().Kind() == reflect.Float64 {
f := v.ToFloat()
switch {
case math.IsNaN(f):
return runtime.ToValue("NaN"), nil
case math.IsInf(f, 1):
return runtime.ToValue("Infinity"), nil
case math.IsInf(f, -1):
return runtime.ToValue("-Infinity"), nil
}
return v, nil
}

So, you don't need Export() at all.
Wdyt? @mstoykov

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems better.

For the record - the idea is that if you are using Export it will do a lot of work in some of the cases. If the end type is flaot64 - that probably is fine. But it also might be a big object that you just fully transformed from JS to go type. And then you just throw it away because it isn't a float64, just to repeat the operation each time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry about that, I resolved the previous conversation thinking I had already implemented the change, totally my bad.
Thanks for the explanation and sorry for the inconvenience!

Let me know if there's anything else I can do on my end.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, no problem @ariasmn, that's why we have a review process, we're all humans and sometimes it happens!

Looks good now! 👌🏻 Thanks! 🙌🏻

@joanlopez joanlopez requested a review from a team July 28, 2025 05:19
Copy link
Contributor

@mstoykov mstoykov left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for the changes and going through figuring it out @ariasmn 🙇

@mstoykov mstoykov merged commit 5bc3231 into grafana:master Jul 31, 2025
39 checks passed
@mstoykov mstoykov added documentation-needed A PR which will need a separate PR for documentation and removed awaiting user waiting for user to respond labels Aug 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: grpc documentation-needed A PR which will need a separate PR for documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants