{"record":{"id":"1db08dc0eb5d3ea2","repo":"OtterMind/Chat2DB","slug":"content-type-must-be-json","errorCode":null,"errorMessage":"Content-Type must be JSON","messagePattern":"Content-Type must be JSON","errorType":"http","errorClass":"RequestError","httpStatus":415,"severity":"error","filePath":"script/github/qq_relay/relay_server.py","lineNumber":202,"sourceCode":"        body = json.dumps(payload, ensure_ascii=False).encode(\"utf-8\")\n        self.send_response(status)\n        self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n        self.send_header(\"Content-Length\", str(len(body)))\n        self.send_header(\"Cache-Control\", \"no-store\")\n        self.send_header(\"X-Content-Type-Options\", \"nosniff\")\n        self.end_headers()\n        self.wfile.write(body)\n\n    def _authorize(self) -> None:\n        expected = f\"Bearer {self.relay_state.config.relay_token}\"\n        supplied = self.headers.get(\"Authorization\", \"\")\n        if not hmac.compare_digest(supplied, expected):\n            raise RequestError(HTTPStatus.UNAUTHORIZED, \"unauthorized\")\n\n    def _read_payload(self) -> Mapping[str, Any]:\n        content_type = self.headers.get(\"Content-Type\", \"\")\n        if not content_type.lower().startswith(\"application/json\"):\n            raise RequestError(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, \"Content-Type must be JSON\")\n        try:\n            content_length = int(self.headers.get(\"Content-Length\", \"\"))\n        except ValueError as error:\n            raise RequestError(HTTPStatus.LENGTH_REQUIRED, \"Content-Length is required\") from error\n        if content_length < 1 or content_length > MAX_REQUEST_BYTES:\n            raise RequestError(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, \"request body is too large\")\n        try:\n            payload = json.loads(self.rfile.read(content_length).decode(\"utf-8\"))\n        except (UnicodeDecodeError, json.JSONDecodeError) as error:\n            raise RequestError(HTTPStatus.BAD_REQUEST, \"request body is not valid JSON\") from error\n        if not isinstance(payload, Mapping):\n            raise RequestError(HTTPStatus.BAD_REQUEST, \"request body must be a JSON object\")\n        return payload\n\n    def _validate_payload(self, payload: Mapping[str, Any]) -> tuple[str, str]:\n        config = self.relay_state.config\n        if payload.get(\"repository\") != config.repository:\n            raise RequestError(HTTPStatus.FORBIDDEN, \"repository is not allowed\")","sourceCodeStart":184,"sourceCodeEnd":220,"githubUrl":"https://github.com/OtterMind/Chat2DB/blob/5ee1e990e73fbcae1969dc554be254fedb3ab888/script/github/qq_relay/relay_server.py#L184-L220","documentation":"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.","triggerScenarios":"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.","commonSituations":"curl without -H \"Content-Type: application/json\"; a client using form encoding; a proxy rewriting Content-Type; a monitoring probe sending text.","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."],"exampleFix":"curl -X POST https://relay.example.com/v1/qq/github \\\n  -H \"Authorization: Bearer $RELAY_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{...}'","handlingStrategy":"validation","validationCode":"content_type = headers.get(\"Content-Type\", \"\")\nif not content_type.lower().startswith(\"application/json\"):\n    headers[\"Content-Type\"] = \"application/json\"","typeGuard":"def is_json_content_type(value: str) -> bool:\n    return value.lower().startswith(\"application/json\")","tryCatchPattern":"from urllib.error import HTTPError\ntry:\n    response = send_relay_message(...)\nexcept HTTPError as error:\n    if error.code == 415:\n        # set Content-Type and retry once\n        pass\n    raise","preventionTips":["Always send Content-Type: application/json.","Check that proxies do not rewrite the header."],"tags":["http","relay","validation","content-type"],"backgroundTag":null,"analyzedSha":"5ee1e990e73fbcae1969dc554be254fedb3ab888","analyzedAt":"2026-08-14T07:05:03.077Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}