OtterMind/Chat2DB · error · RequestError

unauthorized

Error message

unauthorized

What it means

Raised by RelayHandler._authorize (relay_server.py:197) as a RequestError(HTTP 401) when the request's Authorization header does not equal "Bearer <RELAY_TOKEN>". Comparison uses hmac.compare_digest (constant-time) to avoid timing leaks. The notifier's QQ_RELAY_TOKEN must match the relay's RELAY_TOKEN exactly.

Source

Thrown at script/github/qq_relay/relay_server.py:197

class RelayHandler(BaseHTTPRequestHandler):
    relay_state: RelayState
    server_version = "Chat2DBQQRelay/1.0"

    def _send_json(self, status: int, payload: Mapping[str, Any]) -> None:
        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

View on GitHub (pinned to 5ee1e990e7)

Solutions

  1. Synchronize QQ_RELAY_TOKEN in GitHub Actions with RELAY_TOKEN on the relay (both >=32 chars, identical).
  2. Strip trailing whitespace/newlines when storing the secret.
  3. Test with curl using the exact header: -H "Authorization: Bearer $RELAY_TOKEN".
  4. Confirm the scheme is Bearer, not Basic.

Example fix

curl -X POST https://relay.example.com/v1/qq/github \
  -H "Authorization: Bearer $RELAY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"repository":"OtterMind/Chat2DB","delivery_id":"t1","message":"hi"}'
Defensive patterns

Strategy: validation

Validate before calling

import hmac
expected = f"Bearer {relay_token}"
if not hmac.compare_digest(supplied_auth_header, expected):
    raise PermissionError("Authorization token mismatch")

Try / catch

from urllib.error import HTTPError
try:
    response = send_relay_message(relay_url, relay_token, repository, delivery_id, message)
except HTTPError as error:
    if error.code == 401:
        # rotate/sync tokens; do not retry with the same token
        raise RuntimeError("sync QQ_RELAY_TOKEN with RELAY_TOKEN")
    raise

Prevention

When it happens

Trigger: A POST to /v1/qq/github with a missing, malformed, or wrong Bearer token. The bundled notifier sends the right header only if QQ_RELAY_TOKEN equals the server's RELAY_TOKEN.

Common situations: QQ_RELAY_TOKEN (client) and RELAY_TOKEN (server) differ or were rotated on one side only; the token has trailing whitespace/newline when stored as a secret; sending a request without the Authorization header (e.g. curl test); a wrong scheme (Basic instead of Bearer).

Understand the failure class

Related errors


AI-assisted analysis of OtterMind/Chat2DB@5ee1e990e7 (2026-08-14). Data as JSON: /api/errors/531eb137207e0d0d. Report an issue: GitHub.