apache/shenyu · error · ShenyuException
Invalid request body: expected a JSON object
Error message
Invalid request body: expected a JSON object
What it means
After confirming the body is non-empty, toChatCompletionRequest parses it strictly with parseStrict. If the result is null or not a JSON object (e.g. a JSON array, string, or number at the top level), it throws ShenyuException('Invalid request body: expected a JSON object'). OpenAI chat completion payloads must be JSON objects.
Solutions
- Send a JSON object at the top level: {"model":..., "messages":[...]}
- Set Content-Type: application/json on the client request
- Validate the payload shape before sending; parseStrict rejects arrays/scalars by design
- Check for middleware/proxies altering the request body
Example fix
// before
curl -d '[{"role":"user","content":"hi"}]' ...
// after
curl -H 'Content-Type: application/json' -d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}' ... Defensive patterns
Strategy: validation
Validate before calling
try {
JsonNode n = new ObjectMapper().readTree(requestBody);
if (n == null || !n.isObject()) throw new IllegalArgumentException("body must be a JSON object");
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("body is not valid JSON", e);
} Try / catch
try {
ChatCompletionRequest req = OpenAiProtocolAdapter.toChatCompletionRequest(body, stream, config);
} catch (ShenyuException e) {
if (e.getMessage().contains("expected a JSON object")) {
return ResponseEntity.badRequest().body("top-level JSON object required");
}
throw e;
} Prevention
- Send Content-Type: application/json
- Ensure the top-level payload is an object, not an array
- Add a strict JSON-object pre-check in the plugin chain
- Test endpoints with curl using well-formed payloads
When it happens
Trigger: Posting '[...]', '"text"', '42', or any malformed JSON to the AI chat completion endpoint and having it adapted via toChatCompletionRequest.
Common situations: Clients sending wrong Content-Type bodies (form-encoded ending up quoted), sending an array of messages at top level instead of an object with 'messages', proxies mangling the body, curl tests without a proper JSON payload.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Invalid input JSON format
- Invalid JSON format
- Invalid Swagger JSON format:
- Request body must not be empty
- namespaceId is null
AI-assisted analysis of apache/shenyu@567142e072 (2026-09-12).
Data as JSON: /api/errors/87930cfe8c380d51.
Report an issue: GitHub.
Appendix: source
Thrown at shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-common/src/main/java/org/apache/shenyu/plugin/ai/common/protocol/OpenAiProtocolAdapter.java:123
/**
* Parse raw request body directly into ChatCompletionRequest, preserving modeled fields.
* When fallbackConfig is provided, its non-null fields (model, temperature, maxTokens)
* override the client request values, matching the original ChatModel-based fallback behavior
* where the fallback ChatModel's config takes precedence.
*
* @param requestBody the raw JSON request body in OpenAI Chat Completions format
* @param stream whether this is a streaming request
* @param fallbackConfig the fallback config whose non-null fields override client values
* @return a ChatCompletionRequest with modeled fields preserved
*/
public static ChatCompletionRequest toChatCompletionRequest(final String requestBody,
final boolean stream, final AiCommonConfig fallbackConfig) {
if (Objects.isNull(requestBody) || requestBody.isEmpty()) {
throw new ShenyuException("Request body must not be empty");
}
final JsonNode root = parseStrict(requestBody);
if (Objects.isNull(root) || !root.isObject()) {
throw new ShenyuException("Invalid request body: expected a JSON object");
}
final ObjectNode mutableRoot = (ObjectNode) root;
if (Objects.nonNull(fallbackConfig)) {
if (Objects.nonNull(fallbackConfig.getModel()) && !fallbackConfig.getModel().isEmpty()) {
mutableRoot.put(FIELD_MODEL, fallbackConfig.getModel());
}
if (Objects.nonNull(fallbackConfig.getTemperature())) {
mutableRoot.put(FIELD_TEMPERATURE, fallbackConfig.getTemperature());
}
if (Objects.nonNull(fallbackConfig.getMaxTokens())) {
mutableRoot.put(FIELD_MAX_TOKENS, fallbackConfig.getMaxTokens());
mutableRoot.put(FIELD_MAX_COMPLETION_TOKENS, fallbackConfig.getMaxTokens());
}
}
mutableRoot.put(FIELD_STREAM, stream);
View on GitHub (pinned to 567142e072)