Error hierarchy conflates HTTP status concepts

Where does the bug appear (feature/product)?

Cursor SDK

Describe the Bug

The SDK’s exception hierarchy groups errors by incorrect parent classes. Three pairs are affected:

1. PermissionDeniedError(403) inherits AuthenticationError(401)

errors.py line 65: class PermissionDeniedError(AuthenticationError)

This means catching AuthenticationError to handle bad API keys also catches 403 Forbidden errors. A user who typed their key wrong gets the same error-handling code path as someone whose key is valid but lacks access to a resource. Different HTTP concepts, same except block.

2. InternalServerError(500) inherits NetworkError

errors.py line 107: class InternalServerError(NetworkError)

This is the dangerous one. Any retry-on-network-error logic will silently retry 500s. A server crash gets the same treatment as a dropped connection. If you have retry logic with backoff, a 500 from the Cursor API will be retried repeatedly instead of surfaced immediately.

3. BadRequestError(400) inherits ConfigurationError

errors.py line 81: class BadRequestError(ConfigurationError)

Catching ConfigurationError (e.g. “you forgot to set CURSOR_API_KEY”) also catches 400 Bad Request errors (e.g. “your prompt is too long”). These need different user-facing messages.

Steps to Reproduce

from cursor_sdk.errors import (
AuthenticationError, PermissionDeniedError,
NetworkError, InternalServerError,
ConfigurationError, BadRequestError,
)

403 should NOT inherit from 401

print(“PermissionDeniedError extends AuthenticationError:”,
issubclass(PermissionDeniedError, AuthenticationError))

→ True (should be False)

500 should NOT inherit from network error

print(“InternalServerError extends NetworkError:”,
issubclass(InternalServerError, NetworkError))

→ True (should be False)

400 should NOT inherit from config error

print(“BadRequestError extends ConfigurationError:”,
issubclass(BadRequestError, ConfigurationError))

→ True (should be False)

Expected Behavior

CursorSDKError
├── AuthenticationError # 401
├── PermissionDeniedError # 403 (NOT under AuthenticationError)
├── NetworkError # connection failures, timeouts
│ └── APITimeoutError
├── InternalServerError # 500 (NOT under NetworkError)
├── ConfigurationError # missing API key, bad config
├── BadRequestError # 400 (NOT under ConfigurationError)
├── RateLimitError # 429
└── …

Operating System

Windows 10/11

Version Information

N/A cursor-SDK v0.1.6

Additional Information

Enviornment: Python 3.11
Workaround:
Catch leaf types before parent types. Always import and handle the specific errors (PermissionDeniedError, InternalServerError, BadRequestError) BEFORE their incorrectly-assigned parents:

from cursor_sdk import Agent, AgentOptions, LocalAgentOptions
from cursor_sdk.errors import (
PermissionDeniedError, AuthenticationError,
InternalServerError, APITimeoutError, NetworkError,
BadRequestError, ConfigurationError,
)

Auth block: leaf (403) before parent (401)

try:
agent = Agent.create(model=“gpt-5”, local=LocalAgentOptions(cwd=“.”))
except PermissionDeniedError:
print(“Access denied — you don’t have permission for this resource”)
except AuthenticationError:
print(“Bad API key — check CURSOR_API_KEY”)

Dispatch block: leaf (500) before parent (NetworkError)

try:
result = agent.prompt(“do something”)
except InternalServerError:
raise # 500 — don’t retry, surface immediately
except APITimeoutError:
retry() # timeout — safe to retry
except NetworkError:
retry() # connection failure — safe to retry

Config block: leaf (400) before parent (ConfigurationError)

except BadRequestError:
print(“Invalid request — check your prompt or parameters”)
except ConfigurationError:
print(“Configuration issue — check your setup”)

Thank you for shipping the SDK, hope my report is useful:)

Does this stop you from using Cursor

No - Cursor works, but with this issue

Hey, thanks for the report. We don’t often see such a detailed write-up with repro code and a ready workaround.

Confirmed, this is a bug on our side. Three classes in the Python SDK (PermissionDeniedError, BadRequestError, InternalServerError) inherit from the wrong base classes, so an except for the parent type also catches the child. Your point about retry logic that silently retries 500s is valid and important.

Your workaround is correct. Catch the leaf types (specific classes) before their parent types. Until we ship a fix, this is the right way to handle errors correctly.

I’ve filed this internally. Keep in mind the fix will change the inheritance hierarchy, which is a breaking change for code that relies on catching a child class via an except on the parent, so we’ll roll it out carefully. I can’t share an exact ETA yet. Once there’s an update, I’ll reply in the thread.