Skip to content

Fix #772: Improve OpenAI error messages with user-friendly parsing and hints #3869

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions spring-ai-retry/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@
<artifactId>slf4j-api</artifactId>
</dependency>

<!-- Spring Boot JSON starter for Jackson support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>

<!-- test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.ai.retry.openai.OpenAiErrorMessageImprover;

/**
* RetryUtils is a utility class for configuring and handling retry operations. It
Expand Down Expand Up @@ -60,7 +61,8 @@ public void handleError(URI url, HttpMethod method, @NonNull ClientHttpResponse
public void handleError(@NonNull ClientHttpResponse response) throws IOException {
if (response.getStatusCode().isError()) {
String error = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
String message = String.format("%s - %s", response.getStatusCode().value(), error);
String message = OpenAiErrorMessageImprover.improveErrorMessage(error, response.getStatusCode());

/**
* Thrown on 4xx client errors, such as 401 - Incorrect API key provided,
* 401 - You must be a member of an organization to use the API, 429 -
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.ai.retry.openai;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatusCode;
import org.springframework.util.StringUtils;

/**
* Utility class for improving OpenAI API error messages by parsing structured error
* responses and providing user-friendly messages.
*
* @author snowykte0426
* @since 1.1.0
*/
public final class OpenAiErrorMessageImprover {

private static final Logger logger = LoggerFactory.getLogger(OpenAiErrorMessageImprover.class);

private static final ObjectMapper objectMapper = new ObjectMapper();

private OpenAiErrorMessageImprover() {
// Utility class
}

/**
* Attempts to parse and improve an OpenAI error message. If parsing fails, returns a
* fallback message with the original error.
* @param rawError The raw error response from OpenAI API
* @param statusCode The HTTP status code
* @return An improved, user-friendly error message
*/
public static String improveErrorMessage(String rawError, HttpStatusCode statusCode) {
if (!StringUtils.hasText(rawError)) {
return createFallbackMessage("Empty error response", statusCode);
}

try {
// Check if the response looks like JSON
String trimmedError = rawError.trim();
if (trimmedError.startsWith("{") && trimmedError.endsWith("}")) {
OpenAiErrorResponse errorResponse = objectMapper.readValue(trimmedError, OpenAiErrorResponse.class);
if (errorResponse.getError() != null) {
return createImprovedMessage(errorResponse.getError(), statusCode);
}
}
}
catch (Exception e) {
logger.debug("Failed to parse OpenAI error response as JSON: {}", e.getMessage());
}

// Fallback to original behavior if parsing fails
return createFallbackMessage(rawError, statusCode);
}

/**
* Creates an improved error message based on the parsed OpenAI error response.
* @param errorDetail The parsed error detail from OpenAI
* @param statusCode The HTTP status code
* @return A user-friendly error message
*/
private static String createImprovedMessage(OpenAiErrorResponse.ErrorDetail errorDetail,
HttpStatusCode statusCode) {
StringBuilder message = new StringBuilder();

// Add a user-friendly prefix based on status code
message.append(getUserFriendlyPrefix(statusCode));

// Add the main error message
if (StringUtils.hasText(errorDetail.getMessage())) {
message.append(errorDetail.getMessage());
}
else {
message.append("An error occurred with the OpenAI API");
}

// Add helpful context based on error code
String helpfulHint = getHelpfulHint(errorDetail.getCode(), statusCode);
if (StringUtils.hasText(helpfulHint)) {
message.append(" (").append(helpfulHint).append(")");
}

return message.toString();
}

/**
* Creates a fallback error message when parsing fails.
* @param rawError The original error response
* @param statusCode The HTTP status code
* @return A formatted error message
*/
private static String createFallbackMessage(String rawError, HttpStatusCode statusCode) {
return String.format("%s - %s", statusCode.value(), rawError);
}

/**
* Gets a user-friendly prefix based on the HTTP status code.
* @param statusCode The HTTP status code
* @return A user-friendly prefix
*/
private static String getUserFriendlyPrefix(HttpStatusCode statusCode) {
int status = statusCode.value();
if (status == 401) {
return "OpenAI Authentication Error: ";
}
else if (status == 403) {
return "OpenAI Permission Error: ";
}
else if (status == 429) {
return "OpenAI Rate Limit Error: ";
}
else if (status == 500) {
return "OpenAI Server Error: ";
}
else if (status == 503) {
return "OpenAI Service Unavailable: ";
}
else if (statusCode.is4xxClientError()) {
return "OpenAI Client Error: ";
}
else {
return "OpenAI API Error: ";
}
}

/**
* Provides helpful hints based on the error code.
* @param errorCode The OpenAI error code
* @param statusCode The HTTP status code
* @return A helpful hint for the user
*/
private static String getHelpfulHint(String errorCode, HttpStatusCode statusCode) {
if (!StringUtils.hasText(errorCode)) {
return null;
}

if ("invalid_api_key".equals(errorCode)) {
return "Please check your API key at https://platform.openai.com/account/api-keys";
}
else if ("insufficient_quota".equals(errorCode)) {
return "Please check your plan and billing details at https://platform.openai.com/account/billing";
}
else if ("rate_limit_exceeded".equals(errorCode)) {
return "Please wait before making more requests";
}
else if ("model_not_found".equals(errorCode)) {
return "Please verify the model name is correct";
}
else if ("context_length_exceeded".equals(errorCode)) {
return "Please reduce the length of your input";
}
else {
return null;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.ai.retry.openai;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Represents an OpenAI API error response structure. This class is used to parse
* structured error responses from OpenAI API to provide more user-friendly error
* messages.
*
* @author snowykte0426
* @since 1.1.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class OpenAiErrorResponse {

@JsonProperty("error")
private ErrorDetail error;

public OpenAiErrorResponse() {
}

public OpenAiErrorResponse(ErrorDetail error) {
this.error = error;
}

public ErrorDetail getError() {
return this.error;
}

public void setError(ErrorDetail error) {
this.error = error;
}

@JsonIgnoreProperties(ignoreUnknown = true)
public static class ErrorDetail {

@JsonProperty("message")
private String message;

@JsonProperty("type")
private String type;

@JsonProperty("param")
private String param;

@JsonProperty("code")
private String code;

public ErrorDetail() {
}

public ErrorDetail(String message, String type, String param, String code) {
this.message = message;
this.type = type;
this.param = param;
this.code = code;
}

public String getMessage() {
return this.message;
}

public void setMessage(String message) {
this.message = message;
}

public String getType() {
return this.type;
}

public void setType(String type) {
this.type = type;
}

public String getParam() {
return this.param;
}

public void setParam(String param) {
this.param = param;
}

public String getCode() {
return this.code;
}

public void setCode(String code) {
this.code = code;
}

@Override
public String toString() {
return "ErrorDetail{" + "message='" + this.message + '\'' + ", type='" + this.type + '\'' + ", param='"
+ this.param + '\'' + ", code='" + this.code + '\'' + '}';
}

}

@Override
public String toString() {
return "OpenAiErrorResponse{" + "error=" + this.error + '}';
}

}