[GH-ISSUE #41] BUG: 400 Improperly formed request with Claude Code v2.1.6 #26

Closed
opened 2026-02-27 07:17:31 +03:00 by kerem · 4 comments
Owner

Originally created by @myheisenberg on GitHub (Jan 17, 2026).
Original GitHub issue: https://github.com/jwadow/kiro-gateway/issues/41

Kiro Gateway Version

v2.0.0

What happened?

I am using Claude Code v2.1.6. I get "400 Improperly formed request" error especially when using MCP tools or in long conversations.

Debug Logs

error_info.json
response_stream_raw.txt

Originally created by @myheisenberg on GitHub (Jan 17, 2026). Original GitHub issue: https://github.com/jwadow/kiro-gateway/issues/41 ### Kiro Gateway Version v2.0.0 ### What happened? I am using Claude Code v2.1.6. I get "400 Improperly formed request" error especially when using MCP tools or in long conversations. ### Debug Logs [error_info.json](https://github.com/user-attachments/files/24691656/error_info.json) [response_stream_raw.txt](https://github.com/user-attachments/files/24691657/response_stream_raw.txt)
kerem 2026-02-27 07:17:31 +03:00
Author
Owner

@jwadow commented on GitHub (Jan 18, 2026):

Hi @myheisenberg, thanks for the report. But I can't do anything because two key files (request_body.json and kiro_request_body.json) are missing from the issue.

It also seems strange to me that the error_info.json and response_stream_raw.txt files can exist simultaneously with a 400 Improperly formed request error. As far as I remember, in the entire history of the project, I've never seen response_stream_raw.txt even created with this error.

Are you sure you enabled DEBUG_MODE=errors and uploaded the files simultaneously, and not from different requests?

<!-- gh-comment-id:3764873589 --> @jwadow commented on GitHub (Jan 18, 2026): Hi @myheisenberg, thanks for the report. But I can't do anything because two key files (`request_body.json` and `kiro_request_body.json`) are missing from the issue. It also seems strange to me that the `error_info.json` and `response_stream_raw.txt` files can exist simultaneously with a 400 Improperly formed request error. As far as I remember, in the entire history of the project, I've never seen `response_stream_raw.txt` even created with this error. Are you sure you enabled `DEBUG_MODE=errors` and uploaded the files simultaneously, and not from different requests?
Author
Owner

@uratmangun commented on GitHub (Jan 18, 2026):

also got this in opencode i've fixed it myself tho dont know if this works for you but this is what i've done with the help of kiro-cli himself basically the i log all the request inside my sqlite database then i ask ai why is the request from opencode got error and he fix it this the summary:

Fix: Tool Results Lost During Message Merge

Problem

When using the /v1/chat/completions endpoint with conversations containing tool calls, requests were failing with:

Improperly formed request. (reason: None)

Root Cause

The merge_adjacent_messages function in kiro_gateway/converters.py was losing tool_result blocks when merging adjacent user messages.

Scenario That Failed

  1. Assistant makes tool calls (e.g., 3 calls)
  2. Tool results come back (3 tool messages)
  3. User sends a NEW text message (interrupting the tool flow)
  4. The tool results get converted to a user message with tool_result blocks
  5. When merged with the user's text message, extract_text_content was called which discarded the tool_result blocks
  6. Kiro API rejects because toolUses exist without corresponding toolResults

Example from Debug Logs

Request ID 2e96b235 showed:

  • History item 3: Assistant with 3 toolUses
  • History item 4: User message with 0 toolResults (should have had 3!)

The tool results were lost during merge.

Solution

1. Added _extract_tool_result_blocks Helper (converters.py:226-247)

def _extract_tool_result_blocks(content: Any) -> List[Dict[str, Any]]:
    """
    Extracts tool_result blocks from message content.
    """
    if not isinstance(content, list):
        return []
    
    tool_results = []
    for item in content:
        if isinstance(item, dict) and item.get("type") == "tool_result":
            tool_results.append(item)
    
    return tool_results

2. Modified merge_adjacent_messages (converters.py:340-390)

Added preservation of tool_result blocks during merge, similar to existing image preservation:

# CRITICAL: Preserve tool_result blocks from both messages during merging
last_tool_results = _extract_tool_result_blocks(last_content)
current_tool_results = _extract_tool_result_blocks(msg_content)
all_tool_results = last_tool_results + current_tool_results

# Build merged content
new_content = []
if combined_text:
    new_content.append({"type": "text", "text": combined_text})
new_content.extend(all_images)
new_content.extend(all_tool_results)

Files Changed

  • kiro_gateway/converters.py
    • Added _extract_tool_result_blocks function
    • Modified merge_adjacent_messages to preserve tool_result blocks

Testing

Test Request

curl -X POST http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "model": "auto",
    "messages": [
      {"role": "system", "content": "You are a helpful assistant."},
      {"role": "user", "content": "Hello"},
      {"role": "assistant", "content": "Let me check.", "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "test_tool", "arguments": "{}"}}]},
      {"role": "tool", "content": "Tool result here", "tool_call_id": "call_1"},
      {"role": "user", "content": "Thanks, now tell me a joke"}
    ],
    "tools": [{"type": "function", "function": {"name": "test_tool", "description": "A test tool", "parameters": {"type": "object", "properties": {}}}}]
  }'

Verification

Check database logs:

podman cp kiro-openai-gateway:/app/data/accounts.sqlite3 /tmp/accounts.sqlite3
sqlite3 /tmp/accounts.sqlite3 "SELECT request_id, response_status FROM request_logs ORDER BY created_at DESC LIMIT 5"

Verify toolResults in kiro_payload:

sqlite3 /tmp/accounts.sqlite3 "SELECT kiro_payload FROM request_logs WHERE request_id='<id>'" | python3 -c "
import sys, json
d = json.load(sys.stdin)
ctx = d['conversationState']['currentMessage']['userInputMessage'].get('userInputMessageContext', {})
print('toolResults:', ctx.get('toolResults', []))
"

Rebuild & Deploy

im putting it inside a container

kiro-openai-gateway.cobacoba
podman build -t localhost/kiro-openai-gateway_kiro-gateway:latest .
systemctl --user restart kiro-openai-gateway
  • The code already had similar logic for preserving images during merge (_extract_images_from_content)
  • Kiro API requires toolResults to match toolUses - orphaned tool uses cause 400 errors
  • There's also existing logic to strip orphaned toolUses from the last assistant message (lines 1019-1040 in converters.py)

i dont have improperly formatted request because of this

<!-- gh-comment-id:3764877829 --> @uratmangun commented on GitHub (Jan 18, 2026): also got this in opencode i've fixed it myself tho dont know if this works for you but this is what i've done with the help of kiro-cli himself basically the i log all the request inside my sqlite database then i ask ai why is the request from opencode got error and he fix it this the summary: # Fix: Tool Results Lost During Message Merge ## Problem When using the `/v1/chat/completions` endpoint with conversations containing tool calls, requests were failing with: ``` Improperly formed request. (reason: None) ``` ## Root Cause The `merge_adjacent_messages` function in `kiro_gateway/converters.py` was losing `tool_result` blocks when merging adjacent user messages. ### Scenario That Failed 1. Assistant makes tool calls (e.g., 3 calls) 2. Tool results come back (3 tool messages) 3. User sends a NEW text message (interrupting the tool flow) 4. The tool results get converted to a user message with `tool_result` blocks 5. When merged with the user's text message, `extract_text_content` was called which discarded the `tool_result` blocks 6. Kiro API rejects because `toolUses` exist without corresponding `toolResults` ### Example from Debug Logs Request ID `2e96b235` showed: - History item 3: Assistant with 3 `toolUses` - History item 4: User message with 0 `toolResults` (should have had 3!) The tool results were lost during merge. ## Solution ### 1. Added `_extract_tool_result_blocks` Helper (converters.py:226-247) ```python def _extract_tool_result_blocks(content: Any) -> List[Dict[str, Any]]: """ Extracts tool_result blocks from message content. """ if not isinstance(content, list): return [] tool_results = [] for item in content: if isinstance(item, dict) and item.get("type") == "tool_result": tool_results.append(item) return tool_results ``` ### 2. Modified `merge_adjacent_messages` (converters.py:340-390) Added preservation of `tool_result` blocks during merge, similar to existing image preservation: ```python # CRITICAL: Preserve tool_result blocks from both messages during merging last_tool_results = _extract_tool_result_blocks(last_content) current_tool_results = _extract_tool_result_blocks(msg_content) all_tool_results = last_tool_results + current_tool_results # Build merged content new_content = [] if combined_text: new_content.append({"type": "text", "text": combined_text}) new_content.extend(all_images) new_content.extend(all_tool_results) ``` ## Files Changed - `kiro_gateway/converters.py` - Added `_extract_tool_result_blocks` function - Modified `merge_adjacent_messages` to preserve tool_result blocks ## Testing ### Test Request ```bash curl -X POST http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <token>" \ -d '{ "model": "auto", "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Let me check.", "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "test_tool", "arguments": "{}"}}]}, {"role": "tool", "content": "Tool result here", "tool_call_id": "call_1"}, {"role": "user", "content": "Thanks, now tell me a joke"} ], "tools": [{"type": "function", "function": {"name": "test_tool", "description": "A test tool", "parameters": {"type": "object", "properties": {}}}}] }' ``` ### Verification Check database logs: ```bash podman cp kiro-openai-gateway:/app/data/accounts.sqlite3 /tmp/accounts.sqlite3 sqlite3 /tmp/accounts.sqlite3 "SELECT request_id, response_status FROM request_logs ORDER BY created_at DESC LIMIT 5" ``` Verify `toolResults` in kiro_payload: ```bash sqlite3 /tmp/accounts.sqlite3 "SELECT kiro_payload FROM request_logs WHERE request_id='<id>'" | python3 -c " import sys, json d = json.load(sys.stdin) ctx = d['conversationState']['currentMessage']['userInputMessage'].get('userInputMessageContext', {}) print('toolResults:', ctx.get('toolResults', [])) " ``` ## Rebuild & Deploy im putting it inside a container ```bash kiro-openai-gateway.cobacoba podman build -t localhost/kiro-openai-gateway_kiro-gateway:latest . systemctl --user restart kiro-openai-gateway ``` ## Related Context - The code already had similar logic for preserving images during merge (`_extract_images_from_content`) - Kiro API requires `toolResults` to match `toolUses` - orphaned tool uses cause 400 errors - There's also existing logic to strip orphaned `toolUses` from the last assistant message (lines 1019-1040 in converters.py) i dont have improperly formatted request because of this
Author
Owner

@myheisenberg commented on GitHub (Jan 18, 2026):

@jwadow

Sorry, I think the first one didn’t work. I ran it again and all the files came through.

kiro_request_body.json
request_body.json
response_stream_raw.txt
app_logs.txt
error_info.json

@uratmangun Thanks, I’ll give it a try.

<!-- gh-comment-id:3765496778 --> @myheisenberg commented on GitHub (Jan 18, 2026): @jwadow Sorry, I think the first one didn’t work. I ran it again and all the files came through. [kiro_request_body.json](https://github.com/user-attachments/files/24697045/kiro_request_body.json) [request_body.json](https://github.com/user-attachments/files/24697046/request_body.json) [response_stream_raw.txt](https://github.com/user-attachments/files/24697044/response_stream_raw.txt) [app_logs.txt](https://github.com/user-attachments/files/24697043/app_logs.txt) [error_info.json](https://github.com/user-attachments/files/24697047/error_info.json) @uratmangun Thanks, I’ll give it a try.
Author
Owner

@jwadow commented on GitHub (Jan 19, 2026):

Hey @myheisenberg, figured out what's going on with your setup. Your MCP tools have super long names that go over Kiro API's 64 character limit. Like mcp__GitHub__check_if_a_person_is_followed_by_the_authenticated_user is 68 chars, mcp__GitHub__check_if_a_repository_is_starred_by_the_authenticated_user is 71 chars, etc.

I added validation so now instead of that cryptic "400 Improperly formed request" you'll get a clear error listing exactly which tools are too long and by how much.

Can't auto-shorten them though because that would break everything. The MCP server expects exact tool names, if we change them the tools won't work at all. You'll need to configure your MCP server to use shorter names, most of them let you customize the prefix or format.

@uratmangun your issue sounds different, that was about tool_results getting lost during message merge. That's already handled in the current code by the merge_adjacent_messages function which preserves tool_results properly.

<!-- gh-comment-id:3766415563 --> @jwadow commented on GitHub (Jan 19, 2026): Hey @myheisenberg, figured out what's going on with your setup. Your MCP tools have super long names that go over Kiro API's 64 character limit. Like `mcp__GitHub__check_if_a_person_is_followed_by_the_authenticated_user` is 68 chars, `mcp__GitHub__check_if_a_repository_is_starred_by_the_authenticated_user` is 71 chars, etc. I added validation so now instead of that cryptic "400 Improperly formed request" you'll get a clear error listing exactly which tools are too long and by how much. Can't auto-shorten them though because that would break everything. The MCP server expects exact tool names, if we change them the tools won't work at all. You'll need to configure your MCP server to use shorter names, most of them let you customize the prefix or format. @uratmangun your issue sounds different, that was about tool_results getting lost during message merge. That's already handled in the current code by the merge_adjacent_messages function which preserves tool_results properly.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/kiro-gateway-jwadow#26
No description provided.