Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ dev = [
"pytest-asyncio>=0.26.0",
"pytest-cov>=6.1.1",
"pytest-pretty>=1.3.0",
"ruff>=0.12.0",
"ruff>=0.14.0",
]

[build-system]
Expand Down Expand Up @@ -71,7 +71,7 @@ include = ["src", "tests"]
disable_error_code = ["empty-body"]

[tool.ruff]
target-version = "py39"
target-version = "py310"

[tool.ruff.lint.isort]
combine-as-imports = true
Expand Down
103 changes: 54 additions & 49 deletions src/nexusrpc/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

from dataclasses import dataclass
from enum import Enum
from typing import Optional, TypeVar
from logging import getLogger
from typing import TypeVar

logger = getLogger(__name__)

InputT = TypeVar("InputT", contravariant=True)
"""Operation input type"""
Expand Down Expand Up @@ -32,46 +35,55 @@ class HandlerError(Exception):
# Raise a bad request error
raise nexusrpc.HandlerError(
"Invalid input provided",
type=nexusrpc.HandlerErrorType.BAD_REQUEST
error_type=nexusrpc.HandlerErrorType.BAD_REQUEST
)

# Raise a retryable internal error
raise nexusrpc.HandlerError(
"Database unavailable",
type=nexusrpc.HandlerErrorType.INTERNAL,
retryable=True
error_type=nexusrpc.HandlerErrorType.INTERNAL,
retryable_override=True
)
"""

def __init__(
self,
message: str,
*,
type: HandlerErrorType,
retryable_override: Optional[bool] = None,
error_type: HandlerErrorType | str,
retryable_override: bool | None = None,
):
"""
Initialize a new HandlerError.

:param message: A descriptive message for the error. This will become
the `message` in the resulting Nexus Failure object.

:param type: The :py:class:`HandlerErrorType` of the error.
:param error_type: The :py:class:`HandlerErrorType` of the error, or a
string representation of the error type. If a string is
provided and doesn't match a known error type, it will
be treated as UNKNOWN and a warning will be logged.

:param retryable_override: Optionally set whether the error should be
retried. By default, the error type is used
to determine this.
"""
super().__init__(message)
self._type = type
self._retryable_override = retryable_override

@property
def retryable_override(self) -> Optional[bool]:
"""
The optional retryability override set when this error was created.
"""
return self._retryable_override
# Handle string error types
if isinstance(error_type, str):
raw_error_type = error_type
try:
error_type = HandlerErrorType[error_type]
except KeyError:
logger.warning(f"Unknown Nexus HandlerErrorType: {error_type}")
error_type = HandlerErrorType.UNKNOWN
else:
raw_error_type = error_type.value

self.error_type = error_type
self.raw_error_type = raw_error_type
self.retryable_override = retryable_override

@property
def retryable(self) -> bool:
Expand All @@ -82,40 +94,28 @@ def retryable(self) -> bool:
error type is used. See
https://github.com/nexus-rpc/api/blob/main/SPEC.md#predefined-handler-errors
"""
if self._retryable_override is not None:
return self._retryable_override

non_retryable_types = {
HandlerErrorType.BAD_REQUEST,
HandlerErrorType.UNAUTHENTICATED,
HandlerErrorType.UNAUTHORIZED,
HandlerErrorType.NOT_FOUND,
HandlerErrorType.CONFLICT,
HandlerErrorType.NOT_IMPLEMENTED,
}
retryable_types = {
HandlerErrorType.REQUEST_TIMEOUT,
HandlerErrorType.RESOURCE_EXHAUSTED,
HandlerErrorType.INTERNAL,
HandlerErrorType.UNAVAILABLE,
HandlerErrorType.UPSTREAM_TIMEOUT,
}
if self._type in non_retryable_types:
return False
elif self._type in retryable_types:
return True
else:
return True

@property
def type(self) -> HandlerErrorType:
"""
The type of handler error.

See :py:class:`HandlerErrorType` and
https://github.com/nexus-rpc/api/blob/main/SPEC.md#predefined-handler-errors.
"""
return self._type
if self.retryable_override is not None:
return self.retryable_override

match self.error_type:
case (
HandlerErrorType.BAD_REQUEST
| HandlerErrorType.UNAUTHENTICATED
| HandlerErrorType.UNAUTHORIZED
| HandlerErrorType.NOT_FOUND
| HandlerErrorType.CONFLICT
| HandlerErrorType.NOT_IMPLEMENTED
):
return False
case (
HandlerErrorType.RESOURCE_EXHAUSTED
| HandlerErrorType.REQUEST_TIMEOUT
| HandlerErrorType.INTERNAL
| HandlerErrorType.UNAVAILABLE
| HandlerErrorType.UPSTREAM_TIMEOUT
| HandlerErrorType.UNKNOWN
):
return True


class HandlerErrorType(Enum):
Expand All @@ -124,6 +124,11 @@ class HandlerErrorType(Enum):
See https://github.com/nexus-rpc/api/blob/main/SPEC.md#predefined-handler-errors
"""

UNKNOWN = "UNKNOWN"
"""
The error type is unknown. Subsequent requests by the client are permissible.
"""

BAD_REQUEST = "BAD_REQUEST"
"""
The handler cannot or will not process the request due to an apparent client error.
Expand Down
8 changes: 4 additions & 4 deletions src/nexusrpc/handler/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def _get_service_handler(self, service_name: str) -> ServiceHandler:
if service is None:
raise HandlerError(
f"No handler for service '{service_name}'.",
type=HandlerErrorType.NOT_FOUND,
error_type=HandlerErrorType.NOT_FOUND,
)
return service

Expand Down Expand Up @@ -376,15 +376,15 @@ def get_operation_handler(self, operation_name: str) -> OperationHandler[Any, An
f"Nexus service definition '{self.service.name}' has no operation "
f"'{operation_name}'. There are {len(self.service.operation_definitions)} operations "
f"in the definition.",
type=HandlerErrorType.NOT_FOUND,
error_type=HandlerErrorType.NOT_FOUND,
)
operation_handler = self.operation_handlers.get(operation_name)
if operation_handler is None:
raise HandlerError(
f"Nexus service implementation '{self.service.name}' has no handler for "
f"operation '{operation_name}'. There are {len(self.operation_handlers)} "
f"available operation handlers.",
type=HandlerErrorType.NOT_FOUND,
error_type=HandlerErrorType.NOT_FOUND,
)
return operation_handler

Expand Down Expand Up @@ -416,7 +416,7 @@ class OperationHandlerMiddleware(ABC):
"""
Middleware for operation handlers.

This should be extended by any operation handler middelware.
This should be extended by any operation handler middleware.
"""

@abstractmethod
Expand Down
66 changes: 48 additions & 18 deletions tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,69 @@

def test_handler_error_retryable_type():
retryable_error_type = HandlerErrorType.RESOURCE_EXHAUSTED
assert HandlerError(
err = HandlerError(
"test",
type=retryable_error_type,
error_type=retryable_error_type,
retryable_override=True,
).retryable
)
assert err.retryable
assert err.error_type == retryable_error_type
assert err.raw_error_type == retryable_error_type.value

assert not HandlerError(
err = HandlerError(
"test",
type=retryable_error_type,
error_type=retryable_error_type,
retryable_override=False,
).retryable
)
assert not err.retryable
assert err.error_type == retryable_error_type
assert err.raw_error_type == retryable_error_type.value

assert HandlerError(
err = HandlerError(
"test",
type=retryable_error_type,
).retryable
error_type=retryable_error_type,
)
assert err.retryable
assert err.error_type == retryable_error_type
assert err.raw_error_type == retryable_error_type.value


def test_handler_error_non_retryable_type():
non_retryable_error_type = HandlerErrorType.BAD_REQUEST
assert HandlerError(
err = HandlerError(
"test",
type=non_retryable_error_type,
error_type=non_retryable_error_type,
retryable_override=True,
).retryable
)
assert err.retryable
assert err.error_type == non_retryable_error_type
assert err.raw_error_type == non_retryable_error_type.value

assert not HandlerError(
err = HandlerError(
"test",
type=non_retryable_error_type,
error_type=non_retryable_error_type,
retryable_override=False,
).retryable
)
assert not err.retryable
assert err.error_type == non_retryable_error_type
assert err.raw_error_type == non_retryable_error_type.value

assert not HandlerError(
err = HandlerError(
"test",
type=non_retryable_error_type,
).retryable
error_type=non_retryable_error_type,
)
assert not err.retryable
assert err.error_type == non_retryable_error_type
assert err.raw_error_type == non_retryable_error_type.value


def test_handler_error_unknown_error_type():
err = HandlerError("test", error_type="SOME_UNKNOWN_TYPE")
assert err.retryable
assert err.error_type == HandlerErrorType.UNKNOWN
assert err.raw_error_type == "SOME_UNKNOWN_TYPE"

err = HandlerError("test", error_type="SOME_UNKNOWN_TYPE", retryable_override=False)
assert not err.retryable
assert err.error_type == HandlerErrorType.UNKNOWN
assert err.raw_error_type == "SOME_UNKNOWN_TYPE"
Loading