-
Notifications
You must be signed in to change notification settings - Fork 25.4k
Refactor response handlers to improve error handling and streamline mid-stream error processing #128923
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
base: main
Are you sure you want to change the base?
Refactor response handlers to improve error handling and streamline mid-stream error processing #128923
Changes from 1 commit
4371bbd
99d4216
9b696be
34cd687
569e351
0a09f00
f4582f3
8ff0bb6
a7320f4
8cc7d72
9f4abba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,10 +12,12 @@ | |
import org.elasticsearch.common.Strings; | ||
import org.elasticsearch.inference.InferenceServiceResults; | ||
import org.elasticsearch.rest.RestStatus; | ||
import org.elasticsearch.xpack.core.inference.results.UnifiedChatCompletionException; | ||
import org.elasticsearch.xpack.inference.external.http.HttpResult; | ||
import org.elasticsearch.xpack.inference.external.request.Request; | ||
import org.elasticsearch.xpack.inference.logging.ThrottlerManager; | ||
|
||
import java.util.Locale; | ||
import java.util.Objects; | ||
import java.util.function.Function; | ||
|
||
|
@@ -34,17 +36,23 @@ public abstract class BaseResponseHandler implements ResponseHandler { | |
public static final String SERVER_ERROR_OBJECT = "Received an error response"; | ||
public static final String BAD_REQUEST = "Received a bad request status code"; | ||
public static final String METHOD_NOT_ALLOWED = "Received a method not allowed status code"; | ||
protected static final String ERROR_TYPE = "error"; | ||
protected static final String STREAM_ERROR = "stream_error"; | ||
|
||
protected final String requestType; | ||
protected final ResponseParser parseFunction; | ||
private final Function<HttpResult, ErrorResponse> errorParseFunction; | ||
private final boolean canHandleStreamingResponses; | ||
|
||
public BaseResponseHandler(String requestType, ResponseParser parseFunction, Function<HttpResult, ErrorResponse> errorParseFunction) { | ||
protected BaseResponseHandler( | ||
String requestType, | ||
ResponseParser parseFunction, | ||
Function<HttpResult, ErrorResponse> errorParseFunction | ||
) { | ||
this(requestType, parseFunction, errorParseFunction, false); | ||
} | ||
|
||
public BaseResponseHandler( | ||
protected BaseResponseHandler( | ||
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. This constructor is not used anywhere but children classes, making it protected for that reason. |
||
String requestType, | ||
ResponseParser parseFunction, | ||
Function<HttpResult, ErrorResponse> errorParseFunction, | ||
|
@@ -109,19 +117,230 @@ private void checkForErrorObject(Request request, HttpResult result) { | |
} | ||
|
||
protected Exception buildError(String message, Request request, HttpResult result) { | ||
var errorEntityMsg = errorParseFunction.apply(result); | ||
return buildError(message, request, result, errorEntityMsg); | ||
var errorResponse = errorParseFunction.apply(result); | ||
return buildError(message, request, result, errorResponse); | ||
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. errorParseFunction creates ErrorResponse instance, not a message. errorEntityMsg variable naming doesn't really make sense here. Fixed. |
||
} | ||
|
||
protected Exception buildError(String message, Request request, HttpResult result, ErrorResponse errorResponse) { | ||
var responseStatusCode = result.response().getStatusLine().getStatusCode(); | ||
return new ElasticsearchStatusException( | ||
errorMessage(message, request, result, errorResponse, responseStatusCode), | ||
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. Removed unused parameter |
||
errorMessage(message, request, errorResponse, responseStatusCode), | ||
toRestStatus(responseStatusCode) | ||
); | ||
} | ||
|
||
protected String errorMessage(String message, Request request, HttpResult result, ErrorResponse errorResponse, int statusCode) { | ||
/** | ||
* Builds an error for a streaming request with a custom error type. | ||
* This method is used when an error response is received from the external service. | ||
* Only streaming requests support this format, and it should be used when the error response. | ||
* | ||
* @param message the error message to include in the exception | ||
* @param request the request that caused the error | ||
* @param result the HTTP result containing the error response | ||
* @param errorResponse the parsed error response from the HTTP result | ||
* @param errorResponseClass the class of the expected error response type | ||
* @return an instance of {@link UnifiedChatCompletionException} with details from the error response | ||
*/ | ||
protected UnifiedChatCompletionException buildChatCompletionError( | ||
String message, | ||
Request request, | ||
HttpResult result, | ||
ErrorResponse errorResponse, | ||
Class<? extends ErrorResponse> errorResponseClass | ||
) { | ||
assert request.isStreaming() : "Only streaming requests support this format"; | ||
var statusCode = result.response().getStatusLine().getStatusCode(); | ||
var errorMessage = errorMessage(message, request, errorResponse, statusCode); | ||
var restStatus = toRestStatus(statusCode); | ||
|
||
return buildChatCompletionError(errorResponse, errorMessage, restStatus, errorResponseClass); | ||
} | ||
|
||
/** | ||
* Builds a {@link UnifiedChatCompletionException} for a streaming request. | ||
* This method is used when an error response is received from the external service. | ||
* Only streaming requests should use this method. | ||
* | ||
* @param errorResponse the error response parsed from the HTTP result | ||
* @param errorMessage the error message to include in the exception | ||
* @param restStatus the REST status code of the response | ||
* @param errorResponseClass the class of the expected error response type | ||
* @return an instance of {@link UnifiedChatCompletionException} with details from the error response | ||
*/ | ||
protected UnifiedChatCompletionException buildChatCompletionError( | ||
ErrorResponse errorResponse, | ||
String errorMessage, | ||
RestStatus restStatus, | ||
Class<? extends ErrorResponse> errorResponseClass | ||
) { | ||
if (errorResponseClass.isInstance(errorResponse)) { | ||
return buildProviderSpecificChatCompletionError(errorResponse, errorMessage, restStatus); | ||
} else { | ||
return buildDefaultChatCompletionError(errorResponse, errorMessage, restStatus); | ||
} | ||
} | ||
|
||
/** | ||
* Builds a custom {@link UnifiedChatCompletionException} for a streaming request. | ||
* This method is called when a specific error response is found in the HTTP result. | ||
* It must be implemented by subclasses to handle specific error response formats. | ||
* Only streaming requests should use this method. | ||
* | ||
* @param errorResponse the error response parsed from the HTTP result | ||
* @param errorMessage the error message to include in the exception | ||
* @param restStatus the REST status code of the response | ||
* @return an instance of {@link UnifiedChatCompletionException} with details from the error response | ||
*/ | ||
protected UnifiedChatCompletionException buildProviderSpecificChatCompletionError( | ||
ErrorResponse errorResponse, | ||
String errorMessage, | ||
RestStatus restStatus | ||
) { | ||
throw new UnsupportedOperationException( | ||
"Custom error handling is not implemented. Please override buildProviderSpecificChatCompletionError method." | ||
); | ||
} | ||
|
||
/** | ||
* Builds a default {@link UnifiedChatCompletionException} for a streaming request. | ||
* This method is used when an error response is received but no specific error handling is implemented. | ||
* Only streaming requests should use this method. | ||
* | ||
* @param errorResponse the error response parsed from the HTTP result | ||
* @param errorMessage the error message to include in the exception | ||
* @param restStatus the REST status code of the response | ||
* @return an instance of {@link UnifiedChatCompletionException} with details from the error response | ||
*/ | ||
protected UnifiedChatCompletionException buildDefaultChatCompletionError( | ||
ErrorResponse errorResponse, | ||
String errorMessage, | ||
RestStatus restStatus | ||
) { | ||
return new UnifiedChatCompletionException( | ||
restStatus, | ||
errorMessage, | ||
createErrorType(errorResponse), | ||
restStatus.name().toLowerCase(Locale.ROOT) | ||
); | ||
} | ||
|
||
/** | ||
* Builds a mid-stream error for a streaming request. | ||
* This method is used when an error occurs while processing a streaming response. | ||
* It must be implemented by subclasses to handle specific error response formats. | ||
* Only streaming requests should use this method. | ||
* | ||
* @param inferenceEntityId the ID of the inference entity | ||
* @param message the error message | ||
* @param e the exception that caused the error, can be null | ||
* @return a {@link UnifiedChatCompletionException} representing the mid-stream error | ||
*/ | ||
public UnifiedChatCompletionException buildMidStreamChatCompletionError(String inferenceEntityId, String message, Exception e) { | ||
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. Do we need this here? It seems like we always override this and call the Can we remove this from the base class and have the methods be private/public within the implementing classes? 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. Agree with your point. Did the needed change. |
||
throw new UnsupportedOperationException( | ||
"Mid-stream error handling is not implemented. Please override buildMidStreamChatCompletionError method." | ||
); | ||
} | ||
|
||
/** | ||
* Builds a mid-stream error for a streaming request with a custom error type. | ||
* This method is used when an error occurs while processing a streaming response and allows for custom error handling. | ||
* Only streaming requests should use this method. | ||
* | ||
* @param inferenceEntityId the ID of the inference entity | ||
* @param message the error message | ||
* @param e the exception that caused the error, can be null | ||
* @param errorResponseClass the class of the expected error response type | ||
* @return a {@link UnifiedChatCompletionException} representing the mid-stream error | ||
*/ | ||
protected UnifiedChatCompletionException buildMidStreamChatCompletionError( | ||
String inferenceEntityId, | ||
String message, | ||
Exception e, | ||
Class<? extends ErrorResponse> errorResponseClass | ||
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. Instead of passing in a class here how about we either:
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. I made the change that would take supplier that returns Class. Approach of passing a function that would take String as parameter doesn't make sense to me because we're capable of determening the exact subclass of ErrorResponse based on ResponseHandler subclass. No string is needed for that purpose. 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. Sorry I wasn't clear, I created a pull request against your repo to give an example: Jan-Kazlouski-elastic#1 I didn't make the changes across all the handlers. Let me know if you think the example will work, if so let's change the rest of the handlers. The example I gave was for 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. I noticed you've used StreamingErrorResponse as the standard for non-mid-stream errors, which is specific to OpenAI and Google's Vertex. But it doesn't align with how other providers handle errors. For some - same error is used for both midstream and not midstream, for some - not, for some - midstream is handled by ancestor class. Even Introducing abstract class would make things more confusing. I really liked your earlier suggestion of moving exception creation into the ErrorResponse itself. This keeps things cleaner because there is no need to mess around with classes. Each error response object simply knows how to generate its own exception. And it is determined by the specific interface. If it can't, or if parsing fails, we fall back to a default. It's much simpler. (Bonus) Maybe now is a good time to revisit the idea of unifying error structures across all providers, as previously suggested? To consider only returned JSON and don't try to parse it into structures and maintain them reflecting potential changes 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. Please do let me know if current approach works. I really like how it simplifies things. |
||
) { | ||
// Extract the error response from the message using the provided method | ||
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. Let me know if commenting here is excessive. |
||
var errorResponse = extractMidStreamChatCompletionErrorResponse(message); | ||
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. I think this class is getting kind of confusing with the various protected methods. I think there are a few places where we can move protected methods into parameters potentially which might make things clearer because we won't need a base class implementation that throws an error. After the parameter change I mentioned above we can call the method/function we passed in. 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. Done. Now methods that were used to throw UnsupportedOperationException are being passed as parameters. |
||
// Check if the error response matches the expected type | ||
if (errorResponseClass.isInstance(errorResponse)) { | ||
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. I think what this is trying to do is figure out if we were able to parse the json string or not. If we were able to parse json string then we'll return the child error class. For the places that I looked, if we can't parse the string we'll return So instead of checking for the object instance I think we can use 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. Checking if object is an instance of class with .isInstance() method is needed to check which subclass of ErrorResponse is actually there, not just to check whether or not structure was found and parsing was successful. errorStructureFound will return true for any successful ErrorResponse and it is important to cast to specific ErrorResponse subclass and not just to any ErrorResponse. |
||
// If it matches, we can build a custom mid-stream error exception | ||
return buildProviderSpecificMidStreamChatCompletionError(inferenceEntityId, errorResponse); | ||
jonathan-buttner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else if (e != null) { | ||
// If the error response does not match, we can still return an exception based on the original throwable | ||
return UnifiedChatCompletionException.fromThrowable(e); | ||
} else { | ||
// If no specific error response is found, we return a default mid-stream error | ||
return buildDefaultMidStreamChatCompletionError(inferenceEntityId, errorResponse); | ||
} | ||
} | ||
|
||
/** | ||
* Builds a custom mid-stream {@link UnifiedChatCompletionException} for a streaming request. | ||
* This method is called when a specific error response is found in the message. | ||
* It must be implemented by subclasses to handle specific error response formats. | ||
* Only streaming requests should use this method. | ||
* | ||
* @param inferenceEntityId the ID of the inference entity | ||
* @param errorResponse the error response parsed from the message | ||
* @return an instance of {@link UnifiedChatCompletionException} with details from the error response | ||
*/ | ||
protected UnifiedChatCompletionException buildProviderSpecificMidStreamChatCompletionError( | ||
String inferenceEntityId, | ||
ErrorResponse errorResponse | ||
) { | ||
throw new UnsupportedOperationException( | ||
"Mid-stream error handling is not implemented for this response handler. " | ||
+ "Please override buildProviderSpecificMidStreamChatCompletionError method." | ||
); | ||
} | ||
|
||
/** | ||
* Builds a default mid-stream error for a streaming request. | ||
* This method is used when no specific error response is found in the message. | ||
* Only streaming requests should use this method. | ||
* | ||
* @param inferenceEntityId the ID of the inference entity | ||
* @param errorResponse the error response extracted from the message | ||
* @return a {@link UnifiedChatCompletionException} representing the default mid-stream error | ||
*/ | ||
protected UnifiedChatCompletionException buildDefaultMidStreamChatCompletionError( | ||
String inferenceEntityId, | ||
ErrorResponse errorResponse | ||
) { | ||
return new UnifiedChatCompletionException( | ||
RestStatus.INTERNAL_SERVER_ERROR, | ||
format("%s for request from inference entity id [%s]", SERVER_ERROR_OBJECT, inferenceEntityId), | ||
createErrorType(errorResponse), | ||
STREAM_ERROR | ||
); | ||
} | ||
|
||
/** | ||
* Extracts the mid-stream error response from the message. | ||
* This method is used to parse the error response from a streaming message. | ||
* It must be implemented by subclasses to handle specific error response formats. | ||
* Only streaming requests should use this method. | ||
* | ||
* @param message the message containing the error response | ||
* @return an {@link ErrorResponse} object representing the mid-stream error | ||
*/ | ||
protected ErrorResponse extractMidStreamChatCompletionErrorResponse(String message) { | ||
throw new UnsupportedOperationException( | ||
"Mid-stream error extraction is not implemented. Please override extractMidStreamChatCompletionErrorResponse method." | ||
); | ||
} | ||
|
||
/** | ||
* Creates a string representation of the error type based on the provided ErrorResponse. | ||
* This method is used to generate a human-readable error type for logging or exception messages. | ||
* | ||
* @param errorResponse the ErrorResponse object | ||
* @return a string representing the error type | ||
*/ | ||
protected static String createErrorType(ErrorResponse errorResponse) { | ||
return errorResponse != null ? errorResponse.getClass().getSimpleName() : "unknown"; | ||
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. Moved this from several handlers so it can be used directly. |
||
} | ||
|
||
protected String errorMessage(String message, Request request, ErrorResponse errorResponse, int statusCode) { | ||
return (errorResponse == null | ||
|| errorResponse.errorStructureFound() == false | ||
|| Strings.isNullOrEmpty(errorResponse.getErrorMessage())) | ||
|
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.
This constructor is not used anywhere but children classes, making it protected for that reason.