RestApiTool: integer / boolean / array header and cookie parameters crash the agent run (httpx: Header value must be str or bytes)

Open
#7,164 0 comments 0 reactions 1 assignee View on GitHub

@llalitkumarrr is already working on this.

Since Sep 18, 2026.

Assessment

This issue has not been assessed yet.

Description

tools

🔴 Required Information

Describe the Bug:

RestApiTool._prepare_request_params() passes header and cookie parameter values to httpx unchanged:

elif param_location == "header":
  header_params[original_k] = v
elif param_location == "cookie":
  cookie_params[original_k] = v

httpx serializes query values itself (5 becomes "5", False becomes "false"), but it only accepts str/bytes for headers and cookies. So when an OpenAPI spec declares an integer, number, boolean, or array header or cookie parameter, which is common in enterprise APIs (X-Page-Size, X-Api-Version, X-Dry-Run, ...), and the model fills it with a value of that type, the request can't be built. The TypeError isn't turned into a tool error, so the whole agent run fails.

This is separate from #7099, which handles None values for the same parameters.

Steps to Reproduce:

  1. pip install google-adk (reproduced on main @ f33d4923 / 2.9.0).
  2. Run the script below: a real Runner plus LlmAgent plus OpenAPIToolset whose spec has an integer and a boolean header parameter. The model is mocked and HTTP goes to an httpx.MockTransport.

Expected Behavior:
The request is sent with X-Page-Size: 50 and X-Include-Archived: false (OpenAPI's default simple style, and how httpx already encodes query values), and the tool result goes back to the model.

Observed Behavior:

  File ".../httpx/_models.py", line 81, in _normalize_header_value
    raise TypeError(f"Header value must be str or bytes, not {type(value)}")
TypeError: Header value must be str or bytes, not <class 'int'>

The exception propagates out of Runner.run_async(). An integer cookie parameter fails the same way (TypeError: expected string or bytes-like object, got 'int' from http.cookiejar). A boolean header would be sent as True/False even if it got through, rather than true/false.

Environment Details:

  • ADK Library Version (pip show google-adk): 2.9.0 (main @ f33d4923)
  • Desktop OS: Windows 11
  • Python Version (python -V): 3.12.10

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: N/A (mocked model; the bug is in request building)

🟡 Optional Information

Minimal Reproduction Code:

import asyncio

import httpx
from google.adk.agents.llm_agent import LlmAgent
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_response import LlmResponse
from google.adk.runners import InMemoryRunner
from google.adk.tools.openapi_tool.openapi_spec_parser.openapi_toolset import OpenAPIToolset
from google.genai import types

spec = {
    "openapi": "3.0.0",
    "info": {"title": "Invoices", "version": "1"},
    "servers": [{"url": "https://erp.example.com"}],
    "paths": {"/invoices": {"get": {
        "operationId": "listInvoices",
        "parameters": [
            {"name": "X-Page-Size", "in": "header", "required": True, "schema": {"type": "integer"}},
            {"name": "X-Include-Archived", "in": "header", "schema": {"type": "boolean"}},
        ],
        "responses": {"200": {"description": "ok"}},
    }}},
}
sent = {}


def handler(request: httpx.Request):
  sent.update(request.headers)
  return httpx.Response(200, json={"invoices": []})


class FakeModel(BaseLlm):
  model: str = "fake"
  calls: int = 0

  async def generate_content_async(self, llm_request, stream=False):
    self.calls += 1
    part = (types.Part.from_function_call(
                name="list_invoices", args={"x_page_size": 50, "x_include_archived": False})
            if self.calls == 1 else types.Part.from_text(text="done"))
    yield LlmResponse(content=types.Content(role="model", parts=[part]))


agent = LlmAgent(
    name="ap_agent",
    model=FakeModel(),
    tools=[OpenAPIToolset(
        spec_dict=spec,
        httpx_client_factory=lambda: httpx.AsyncClient(transport=httpx.MockTransport(handler)),
    )],
)


async def main():
  runner = InMemoryRunner(agent=agent)
  session = await runner.session_service.create_session(app_name=runner.app_name, user_id="u")
  async for _ in runner.run_async(
      user_id="u", session_id=session.id,
      new_message=types.Content(role="user", parts=[types.Part(text="list invoices")])):
    pass
  print("headers sent:", {k: v for k, v in sent.items() if k.startswith("x-")})


asyncio.run(main())

Suggested fix: serialize header and cookie values before handing them to httpx: numbers as str(v), booleans as true/false, arrays comma-joined. Leave other values alone, so None stays #7099's concern. I have a PR ready with tests.

How often has this issue occurred?:

  • Always (100%): any non-string header or cookie parameter value.
Dominant language
Python
Stars
21.6k
Forks
4k
Avg merge
13h 49m
Merged PRs (30d)
10

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from google/adk-python

All issues in google/adk-python

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.