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
fix(#1559): handle api connection err gracefully #1636
fix(#1559): handle api connection err gracefully #1636
Conversation
Hi @frascuchon The error trace now looks like:
Let me know if further changes required. |
Thanks, @Ankush-Chander for your work. Indeed the referenced issue #1559 talks about the dataset load with unexpected errors. If you want to take a look, you could reproduce the error with the following code snippet: import rubrix as rb
rb.log(name="mock-ds", records=rb.TextClassificationRecord(text="My text"))
rb.load(name="mock-ds") # The load works fine
rb.load(name="mock-ds", query="!!") # Here an error will be raise since a malformed query. The error should be related to how the client handles the response, but I have little time to go deeper. And again, thanks for your time! |
Thanks @frascuchon! I am able to replicate the bug with above steps. |
Hi @frascuchon , So I have handled the exception on the client side. The error trace currently looks like:
Reference: |
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.
Thanks for your work @Ankush-Chander
I think we need to rethink it. Let me have some thoughts about it and then let's discuss the solution.
src/rubrix/client/sdk/commons/api.py
Outdated
except httpx.RemoteProtocolError as err: | ||
raise Exception(f"Malformed query!") from None |
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.
So, that's mean that all error of this kind received from the server will be considered as a malformed query, but we can have errors of different nature, right?
Maybe, we should work on an error mechanism exchange when streaming has already started. Let's me investigate and make you a design proposal, ok?
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.
Hi @frascuchon
Thanks for the review.
So, that's mean that all error of this kind received from the server will be considered as a malformed query, but we can have errors of different nature, right?
RemoteProtocolError comes mostly when protocol is violated by the server for eg: when it"s send the malformed HTTP. Error of different nature are most likely to be captured via explicit non 2XX error codes being sent back from server. Hence I attributed this particular exception to "Malformed query" but definitely there can be some other cases which might lead to same exception.
I suppressed the exception(raise Exception(f"Malformed query!") from None
) here because the original error trace was nested, too verbose and not actionable by the user.
Maybe, we should work on an error mechanism exchange when streaming has already started. Let's me investigate and make you a design proposal, ok?
Design proposal would be very helpful. Looking forward to it.
src/rubrix/client/sdk/users/api.py
Outdated
except httpx.ConnectError as err: | ||
raise Exception(f"Could not connect to api_url:{client.base_url}") from None |
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.
Here, could we include some details about the connection error ??
Hi @Ankush-Chander. I hope you're well. I've been thinking about the solution and I will my thoughts with you. Tell me if make sense for you. The problem is based on how the data streaming is handled in the try:
for batch in grouper(
n=chunk_size,
iterable=stream,
):
filtered_records = filter(lambda r: r is not None, batch)
yield "\n".join(
map(
lambda r: r.json(by_alias=True, exclude_none=True), filtered_records
)
) + "\n"
except Exception as error:
yield errors.exception_to_rubrix_error(err) # imported from rubrix.server.errors If the error occurs at the beginning of streaming data, the response sent to the client will contain a standard HTTP error response (the We can start by including the block What do you think about it? |
Hi @frascuchon, Thanks |
Hi @frascuchon So if we yield an exception from Here are some options I thought about(given the fact there is not much I could do in the
|
I prefer option 2 since clients will be aware that something was wrong on the server side. Then, we can adapt the client to handle those situations with error handling during response parsing: try:
parsed_responses = []
for r in response.iter_lines():
parsed_responses.append(data_type(**json.loads(r)))
return Response(
status_code=response.status_code,
content=b"",
headers=response.headers,
parsed=parsed_response,
)
except Exception as err:
raise GenericApiError(message="Cannot process response", record=r, error=err) Does make sense for you? Note: to normalize the error format sent by the server you can use the |
Hi @frascuchon
Please note exception raised in generator function can"t be caught normally on client side so I had to apply try catch where its being parsed and converted into standard type. Error message for end user now looks like:
Please suggest. |
I see. Yes, indeed we need to handle several aspects regarding the response flow (capture the error, encode as bytes, send as the last response message). Thinking a bit more about it, I found a solution that could simplify this flow by adding a few lines of code: Extending the from starlette.responses import JSONResponse, StreamingResponse
from starlette.types import Send
from rubrix.server.errors import APIErrorHandler
class StreamingResponseWithErrorHandling(StreamingResponse):
async def stream_response(self, send: Send) -> None:
try:
return await super().stream_response(send)
except Exception as ex:
json_response: JSONResponse = await APIErrorHandler.common_exception_handler(send, error=ex)
await send({"type": "http.response.body", "body": json_response.body, "more_body": False}) Here, if there is some error during the streaming, the server communication will end by sending a last message including the normalized rubrix error response body. By having this, the only change we need to apply in the server part is to change the type of response we send to the client on a data stream. On the other hand, the client must handle the received error as you propose. Just take into account that the rendered error has the following schema: # This is just an example. Keep in mind the message structure
{
"detail": {
"code": "rubrix.api.errors::ValidationError",
"params": {
"extra": "error parameters"
}
}
} You should readjust the client error handling that you've implemented. Anyway, great work !!! Thanks a lot and sorry for all this back and forth in running the solution |
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.
👍
Closes #1559 Thanks to @Ankush-Chander (cherry picked from commit 4ab30b9)
Closes #1559 Thanks to @Ankush-Chander (cherry picked from commit 4ab30b9)
Closes #1559 Thanks to @Ankush-Chander (cherry picked from commit 4ab30b9)
Please refer: #1559
Changes made:
rb.load
#1559)