-
Notifications
You must be signed in to change notification settings - Fork 9
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
Troubleshoot and fix issues in grpc-web JS conformance client #831
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,15 +36,36 @@ import { | |
} from "grpc-web"; | ||
|
||
// The main entry point into the browser code running in Puppeteer/headless Chrome. | ||
// This function is invoked by the page.evalulate call in grpcwebclient. | ||
// This function is invoked by the page.evaluate call in grpcwebclient. | ||
async function runTestCase(data: number[]): Promise<number[]> { | ||
const request = ClientCompatRequest.deserializeBinary(new Uint8Array(data)); | ||
|
||
const result = await invoke(request); | ||
const rpcResult = invoke(request); | ||
const timeout = new Promise<ClientResponseResult>((_, reject) => { | ||
// Fail if we still don't have a response after 15 seconds | ||
// so that user can at least see exactly which test case timed out. | ||
setTimeout(() => { | ||
reject(new Error("promise never resolved after 15s!")); | ||
}, 15*1000); | ||
}); | ||
const result = await Promise.race([rpcResult, timeout]); | ||
|
||
return Array.from(result.serializeBinary()); | ||
} | ||
|
||
function addErrorListeners() { | ||
window.addEventListener("error", function (e) { | ||
// @ts-ignore | ||
window.log("ERROR: uncaught error in browser: " + e.error.filename + ":" + e.error.lineno + ": " + e.message); | ||
return false; | ||
}) | ||
window.addEventListener("unhandledrejection", function (e) { | ||
// @ts-ignore | ||
window.log("ERROR: unhandled promise failure in browser: " + e.reason); | ||
return false; | ||
}) | ||
} | ||
|
||
function invoke(req: ClientCompatRequest) { | ||
const client = createClient(req); | ||
switch (req.getMethod()) { | ||
|
@@ -117,10 +138,13 @@ function convertGrpcToProtoError(rpcErr: RpcError): ProtoError { | |
err.setCode(convertStatusCodeToCode(rpcErr.code)); | ||
err.setMessage(rpcErr.message); | ||
|
||
const value = rpcErr.metadata["grpc-status-details-bin"]; | ||
if (value) { | ||
const status = Status.deserializeBinary(stringToUint8Array(atob(value))); | ||
err.setDetailsList(status.getDetailsList()); | ||
let md = rpcErr.metadata; | ||
if (md !== undefined) { | ||
const value = md["grpc-status-details-bin"]; | ||
if (value) { | ||
const status = Status.deserializeBinary(stringToUint8Array(atob(value))); | ||
err.setDetailsList(status.getDetailsList()); | ||
} | ||
} | ||
|
||
return err; | ||
|
@@ -194,6 +218,15 @@ async function unary( | |
(err: RpcError, response: UnaryResponse) => { | ||
if (err !== null) { | ||
resp.setError(convertGrpcToProtoError(err)); | ||
let md = err.metadata; | ||
if (md !== undefined) { | ||
resp.setResponseTrailersList(convertMetadataToHeader(md)); | ||
} | ||
// Ideally, we'd complete the promise from the "end" event. However, | ||
// most RPCs that result in an RPC error (as of 3/15/2024, 50 out of | ||
// 57 failed RPCs) do not produce an "end" event after the callback | ||
// is invoked with an error. | ||
res(resp); | ||
} else { | ||
const payload = response.getPayload(); | ||
if (payload !== undefined) { | ||
|
@@ -212,13 +245,22 @@ async function unary( | |
|
||
// Response trailers (i.e. trailing metadata) are sent in the 'status' event | ||
result.on("status", (status: GrpcWebStatus) => { | ||
// One might expect that the "status" event is always delivered (since | ||
// consistency would make it much easier to implement interceptors or | ||
// decorators, to instrument all RPCs with cross-cutting concerns, like | ||
// metrics, logging, etc). But one would be wrong: as of 3/15/2024, there | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤣 |
||
// are 2 cases where the "status" event is never delivered (both cases | ||
// are RPC failures). | ||
const md = status.metadata; | ||
if (md !== undefined) { | ||
resp.setResponseTrailersList(convertMetadataToHeader(md)); | ||
res(resp); | ||
} | ||
}); | ||
|
||
result.on("end", () => { | ||
res(resp); | ||
}); | ||
|
||
return prom; | ||
} | ||
|
||
|
@@ -266,18 +308,31 @@ async function serverStream( | |
}); | ||
stream.on("error", (err: RpcError) => { | ||
resp.setError(convertGrpcToProtoError(err)); | ||
let md = err.metadata; | ||
if (md !== undefined) { | ||
resp.setResponseTrailersList(convertMetadataToHeader(md)); | ||
} | ||
// Ideally, we'd complete the promise from the "end" event. However, there | ||
// are some RPCs that result in an RPC error (as of 3/15/2024, 3 out of 44 | ||
// failed RPCs) that do not produce an "end" event after the "error" event. | ||
res(resp); | ||
}); | ||
|
||
// Response trailers (i.e. trailing metadata) are sent in the 'status' event | ||
stream.on("status", (status: GrpcWebStatus) => { | ||
// One might expect that the "status" event is always delivered (since | ||
// consistency would make it much easier to implement interceptors or | ||
// decorators, to instrument all RPCs with cross-cutting concerns, like | ||
// metrics, logging, etc). But one would be wrong: as of 3/15/2024, there | ||
// is one case (out of 62 total RPCs) where the "status" event is never | ||
// delivered for a streaming call. | ||
const md = status.metadata; | ||
if (md !== undefined) { | ||
resp.setResponseTrailersList(convertMetadataToHeader(md)); | ||
} | ||
}); | ||
|
||
stream.on("end", function () { | ||
stream.on("end", () => { | ||
res(resp); | ||
}); | ||
|
||
|
@@ -337,3 +392,5 @@ async function unimplemented( | |
|
||
// @ts-ignore | ||
window.runTestCase = runTestCase; | ||
// @ts-ignore | ||
window.addErrorListeners = addErrorListeners; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, the test cases don't actually cause anything to crash, per se. What was happening is that a test case was taking too long (test runner imposes a 20 second timeout). After that time elapsed, the test runner would attempt to stop the client by sending it a SIGTERM. That signal was then propagated to the Chrome child process (maybe
node
does that automatically, or maybe puppeteer adds some sort of shutdown hook or signal handler?). So Chrome then terminated, which causes the code that was awaiting the result of the hung test to unblock with an error. The script would then try to shutdown, and log a nasty error when it tried to callpage.close()
on a browser that was already disconnected/killed. I had misinterpreted that nasty error to mean maybe the browser had crashed, vs. it being caused by the test runner trying to stop the program under test.