OtterMind/Chat2DB · error · RequestError
Content-Type must be JSON
Error message
Content-Type must be JSON
What it means
Raised by RelayHandler._read_payload (relay_server.py:202) as a RequestError(HTTP 415) when the Content-Type header does not start with "application/json" (case-insensitive). The relay only accepts JSON, so any other media type (or a missing Content-Type) is rejected before the body is read.
Source
Thrown at script/github/qq_relay/relay_server.py:202
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.end_headers()
self.wfile.write(body)
def _authorize(self) -> None:
expected = f"Bearer {self.relay_state.config.relay_token}"
supplied = self.headers.get("Authorization", "")
if not hmac.compare_digest(supplied, expected):
raise RequestError(HTTPStatus.UNAUTHORIZED, "unauthorized")
def _read_payload(self) -> Mapping[str, Any]:
content_type = self.headers.get("Content-Type", "")
if not content_type.lower().startswith("application/json"):
raise RequestError(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, "Content-Type must be JSON")
try:
content_length = int(self.headers.get("Content-Length", ""))
except ValueError as error:
raise RequestError(HTTPStatus.LENGTH_REQUIRED, "Content-Length is required") from error
if content_length < 1 or content_length > MAX_REQUEST_BYTES:
raise RequestError(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, "request body is too large")
try:
payload = json.loads(self.rfile.read(content_length).decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as error:
raise RequestError(HTTPStatus.BAD_REQUEST, "request body is not valid JSON") from error
if not isinstance(payload, Mapping):
raise RequestError(HTTPStatus.BAD_REQUEST, "request body must be a JSON object")
return payload
def _validate_payload(self, payload: Mapping[str, Any]) -> tuple[str, str]:
config = self.relay_state.config
if payload.get("repository") != config.repository:
raise RequestError(HTTPStatus.FORBIDDEN, "repository is not allowed")View on GitHub (pinned to 5ee1e990e7)
Solutions
- Send Content-Type: application/json on every request.
- If using curl, add -H "Content-Type: application/json".
- Check any proxy in front of the relay that may strip or rewrite the header.
Example fix
curl -X POST https://relay.example.com/v1/qq/github \
-H "Authorization: Bearer $RELAY_TOKEN" \
-H "Content-Type: application/json" \
-d '{...}' Defensive patterns
Strategy: validation
Validate before calling
content_type = headers.get("Content-Type", "")
if not content_type.lower().startswith("application/json"):
headers["Content-Type"] = "application/json" Type guard
def is_json_content_type(value: str) -> bool:
return value.lower().startswith("application/json") Try / catch
from urllib.error import HTTPError
try:
response = send_relay_message(...)
except HTTPError as error:
if error.code == 415:
# set Content-Type and retry once
pass
raise Prevention
- Always send Content-Type: application/json.
- Check that proxies do not rewrite the header.
When it happens
Trigger: A POST with Content-Type absent or set to text/plain, application/x-www-form-urlencoded, multipart/form-data, etc. The bundled notifier always sends application/json, so this is typically a manual client or a misconfigured integration.
Common situations: curl without -H "Content-Type: application/json"; a client using form encoding; a proxy rewriting Content-Type; a monitoring probe sending text.
Related errors
- Content-Length is required
- request body is too large
- request body is not valid JSON
- A delivery ID is required for relay deduplication
- QQ_GROUP_ID must be a numeric QQ group number
AI-assisted analysis of OtterMind/Chat2DB@5ee1e990e7 (2026-08-14).
Data as JSON: /api/errors/1db08dc0eb5d3ea2.
Report an issue: GitHub.