Skip to content

A missing API key returns 403, but the docs say 401, and the SDKs then raise PermissionDenied instead of AuthenticationError #8

Open

Description

@vbcherepanov

A missing API key returns 403, but the docs say 401, and the SDKs then raise PermissionDenied instead of AuthenticationError

What the docs say (API reference → Errors): 401 Unauthorized — "Missing or invalid API key. Check the Authorization header."

What the API returns (checked 2026-09-21):

# no Authorization header
curl -s -w " HTTP %{http_code}\n" https://api.typesafe.ai/v1/systemone -H "Content-Type: application/json" -d '{}'
# empty bearer token
curl -s -w " HTTP %{http_code}\n" https://api.typesafe.ai/v1/models -H "Authorization: Bearer "
# wrong key
curl -s -w " HTTP %{http_code}\n" https://api.typesafe.ai/v1/models -H "Authorization: Bearer ts_invalid"
{"detail":{"error_type":"authentication_error","message":"Must supply an API key! Check your request and try again."}} HTTP 403
{"detail":{"error_type":"authentication_error","message":"Must supply an API key! Check your request and try again."}} HTTP 403
{"detail":{"error_type":"authentication_error","message":"Cannot authenticate with the server. Please check your API key and try again."}} HTTP 401

So a missing key gets 403 while an invalid key gets 401, although both carry error_type: "authentication_error".

Why it matters

The Python SDK (errors.py) and the JS SDK (errors.ts) map exceptions by status only. A deployment that forgot to set TYPESAFE_API_KEY raises PermissionDeniedError, and code that catches AuthenticationError for "fix your key" handling misses it.

Suggested fix

  • Return 401 for a missing key, as documented. Or list 403 in the error table as "no API key supplied".
  • Optionally, have the SDKs map detail.error_type == "authentication_error" to AuthenticationError regardless of status. This is more robust than keying on the status alone.

Activity

  1. aoright commented on Sep 23, 2026

    @aoright

    I traced the error mapping paths across both official SDKs to see how this impacts client code:

    1. Current SDK Exception Mapping

    Both SDKs currently resolve error subclasses exclusively by HTTP status code:

    • Python SDK (src/typesafe_sdk/_core/errors.py:198-200):
      STATUS_ERROR_TYPES: dict[int, type[TypeSafeAPIError]] = {
          400: TypeSafeBadRequestError,
          401: TypeSafeAuthenticationError,
          403: TypeSafePermissionDeniedError,
          ...
      }
      
      def api_error(status: int, body: object, headers: httpx2.Headers, endpoint: str | None = None) -> TypeSafeAPIError:
          error_type = STATUS_ERROR_TYPES.get(status, ...)
          return error_type(status, body, headers, endpoint=endpoint)
    • JavaScript SDK (src/errors.ts:69-79):
      static fromResponse(status: number, body: unknown, headers: Headers): APIError {
        if (status === 400) return new BadRequestError(status, body, headers);
        if (status === 401) return new AuthenticationError(status, body, headers);
        if (status === 403) return new PermissionDeniedError(status, body, headers);
        ...
      }

    2. Client Impact

    Because the server responds with HTTP 403 for missing or unsupplied credentials with the body:

    {"detail":{"error_type":"authentication_error","message":"Must supply an API key! Check your request and try again."}}

    The client raises PermissionDeniedError (or TypeSafePermissionDeniedError). Standard error-handling middleware catching AuthenticationError (e.g. to prompt the user to re-authenticate or log credential misconfigurations) is bypassed, and the error falls through to generic permission/authorization failure handlers.

    3. Suggested Fix

    In addition to returning HTTP 401 at the gateway for unauthenticated requests as documented in the API reference, both SDKs can inspect the structured error payload in api_error / APIError.fromResponse:
    If body.detail.error_type === "authentication_error" (or body.error_type === "authentication_error"), map to AuthenticationError / TypeSafeAuthenticationError regardless of whether the status is 401 or 403. This protects clients against gateway differences while remaining compatible with status-only fallbacks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions