Skip to content

exception_handlers

CONFIG: ServerConfig = ServerConfig() module-attribute

This singleton loads the config from a hierarchy of sources (see customise_sources) and makes it importable in the server code.

LOGGER = logging.getLogger('optimade') module-attribute

OPTIMADE_EXCEPTIONS: Iterable[tuple[type[Exception], Callable[[Request, Exception], JSONAPIResponse]]] = [(StarletteHTTPException, http_exception_handler), (OptimadeHTTPException, http_exception_handler), (RequestValidationError, request_validation_exception_handler), (ValidationError, validation_exception_handler), (VisitError, grammar_not_implemented_handler), (NotImplementedError, not_implemented_handler), (Exception, general_exception_handler)] module-attribute

A tuple of all pairs of exceptions and handler functions that allow for appropriate responses to be returned in certain scenarios according to the OPTIMADE specification.

To use these in FastAPI app code:

from fastapi import FastAPI
app = FastAPI()
for exception, handler in OPTIMADE_EXCEPTIONS:
    app.add_exception_handler(exception, handler)

BadRequest

Bases: OptimadeHTTPException

400 Bad Request

Source code in optimade/exceptions.py
57
58
59
60
61
class BadRequest(OptimadeHTTPException):
    """400 Bad Request"""

    status_code: int = 400
    title: str = "Bad Request"

ErrorResponse

Bases: Response

errors MUST be present and data MUST be skipped

Source code in optimade/models/responses.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class ErrorResponse(Response):
    """errors MUST be present and data MUST be skipped"""

    meta: Annotated[
        ResponseMeta,
        StrictField(description="A meta object containing non-standard information."),
    ]
    errors: Annotated[
        list[OptimadeError],
        StrictField(
            description="A list of OPTIMADE-specific JSON API error objects, where the field detail MUST be present.",
            uniqueItems=True,
        ),
    ]

    @model_validator(mode="after")
    def data_must_be_skipped(self) -> "ErrorResponse":
        if self.data or "data" in self.model_fields_set:
            raise ValueError("data MUST be skipped for failures reporting errors.")
        return self

model_config = ConfigDict(json_encoders={datetime: lambda : v.astimezone(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')}) class-attribute instance-attribute

The specification mandates that datetimes must be encoded following RFC3339, which does not support fractional seconds, thus they must be stripped in the response. This can cause issues when the underlying database contains fields that do include microseconds, as filters may return unexpected results.

ErrorSource

Bases: BaseModel

an object containing references to the source of the error

Source code in optimade/models/jsonapi.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
class ErrorSource(BaseModel):
    """an object containing references to the source of the error"""

    pointer: Annotated[
        Optional[str],
        StrictField(
            description="a JSON Pointer [RFC6901] to the associated entity in the request document "
            '[e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute].',
        ),
    ] = None
    parameter: Annotated[
        Optional[str],
        StrictField(
            description="a string indicating which URI query parameter caused the error.",
        ),
    ] = None

JSONAPIResponse

Bases: JSONResponse

This class simply patches fastapi.responses.JSONResponse to use the JSON:API 'application/vnd.api+json' MIME type.

Source code in optimade/server/routers/utils.py
41
42
43
44
45
46
47
class JSONAPIResponse(JSONResponse):
    """This class simply patches `fastapi.responses.JSONResponse` to use the
    JSON:API 'application/vnd.api+json' MIME type.

    """

    media_type = "application/vnd.api+json"

OptimadeError

Bases: Error

detail MUST be present

Source code in optimade/models/optimade_json.py
126
127
128
129
130
131
132
133
134
class OptimadeError(jsonapi.Error):
    """detail MUST be present"""

    detail: Annotated[
        str,
        StrictField(
            description="A human-readable explanation specific to this occurrence of the problem.",
        ),
    ]

OptimadeHTTPException

Bases: Exception, ABC

This abstract class can be subclassed to define HTTP responses with the desired status codes, and detailed error strings to represent in the JSON:API error response.

This class closely follows the starlette.HTTPException without requiring it as a dependency, so that such errors can also be raised from within client code.

Attributes:

Name Type Description
status_code int

The HTTP status code accompanying this exception.

title str

A descriptive title for this exception.

detail Optional[str]

An optional string containing the details of the error.

Source code in optimade/exceptions.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class OptimadeHTTPException(Exception, ABC):
    """This abstract class can be subclassed to define
    HTTP responses with the desired status codes, and
    detailed error strings to represent in the JSON:API
    error response.

    This class closely follows the `starlette.HTTPException` without
    requiring it as a dependency, so that such errors can also be
    raised from within client code.

    Attributes:
        status_code: The HTTP status code accompanying this exception.
        title: A descriptive title for this exception.
        detail: An optional string containing the details of the error.

    """

    status_code: int
    title: str
    detail: Optional[str] = None
    headers: Optional[dict[str, Any]] = None

    def __init__(
        self, detail: Optional[str] = None, headers: Optional[dict] = None
    ) -> None:
        if self.status_code is None:
            raise AttributeError(
                "HTTPException class {self.__class__.__name__} is missing required `status_code` attribute."
            )
        self.detail = detail
        self.headers = headers

    def __str__(self) -> str:
        return self.detail if self.detail is not None else self.__repr__()

    def __repr__(self) -> str:
        class_name = self.__class__.__name__
        return f"{class_name}(status_code={self.status_code!r}, detail={self.detail!r})"

general_exception(request, exc, status_code=500, errors=None)

Handle an exception

Parameters:

Name Type Description Default
request Request

The HTTP request resulting in the exception being raised.

required
exc Exception

The exception being raised.

required
status_code int

The returned HTTP status code for the error response.

500
errors Optional[list[OptimadeError]]

List of error resources as defined in the OPTIMADE specification.

None

Returns:

Type Description
JSONAPIResponse

A JSON HTTP response based on ErrorResponse.

Source code in optimade/server/exception_handlers.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def general_exception(
    request: Request,
    exc: Exception,
    status_code: int = 500,  # A status_code in `exc` will take precedence
    errors: Optional[list[OptimadeError]] = None,
) -> JSONAPIResponse:
    """Handle an exception

    Parameters:
        request: The HTTP request resulting in the exception being raised.
        exc: The exception being raised.
        status_code: The returned HTTP status code for the error response.
        errors: List of error resources as defined in
            [the OPTIMADE specification](https://github.com/Materials-Consortia/OPTIMADE/blob/develop/optimade.rst#json-response-schema-common-fields).

    Returns:
        A JSON HTTP response based on [`ErrorResponse`][optimade.models.responses.ErrorResponse].

    """
    debug_info = {}
    if CONFIG.debug:
        tb = "".join(
            traceback.format_exception(type(exc), value=exc, tb=exc.__traceback__)
        )
        LOGGER.error("Traceback:\n%s", tb)
        debug_info[f"_{CONFIG.provider.prefix}_traceback"] = tb

    try:
        http_response_code = int(exc.status_code)  # type: ignore[attr-defined]
    except AttributeError:
        http_response_code = int(status_code)

    try:
        title = str(exc.title)  # type: ignore[attr-defined]
    except AttributeError:
        title = str(exc.__class__.__name__)

    try:
        detail = str(exc.detail)  # type: ignore[attr-defined]
    except AttributeError:
        detail = str(exc)

    if errors is None:
        errors = [OptimadeError(detail=detail, status=http_response_code, title=title)]

    response = ErrorResponse(
        meta=meta_values(
            url=request.url,
            data_returned=0,
            data_available=0,
            more_data_available=False,
            schema=CONFIG.schema_url,
            **debug_info,
        ),
        errors=errors,
    )

    return JSONAPIResponse(
        status_code=http_response_code,
        content=jsonable_encoder(response, exclude_unset=True),
    )

general_exception_handler(request, exc)

Catch all Python Exceptions not handled by other exception handlers

Pass-through directly to general_exception().

Parameters:

Name Type Description Default
request Request

The HTTP request resulting in the exception being raised.

required
exc Exception

The exception being raised.

required

Returns:

Type Description
JSONAPIResponse

A JSON HTTP response through general_exception().

Source code in optimade/server/exception_handlers.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def general_exception_handler(request: Request, exc: Exception) -> JSONAPIResponse:
    """Catch all Python Exceptions not handled by other exception handlers

    Pass-through directly to [`general_exception()`][optimade.server.exception_handlers.general_exception].

    Parameters:
        request: The HTTP request resulting in the exception being raised.
        exc: The exception being raised.

    Returns:
        A JSON HTTP response through [`general_exception()`][optimade.server.exception_handlers.general_exception].

    """
    return general_exception(request, exc)

grammar_not_implemented_handler(request, exc)

Handle an error raised by Lark during filter transformation

All errors raised during filter transformation are wrapped in the Lark VisitError. According to the OPTIMADE specification, these errors are repurposed to be 501 NotImplementedErrors.

For special exceptions, like BadRequest, we pass-through to general_exception(), since they should not return a 501 NotImplementedError.

Parameters:

Name Type Description Default
request Request

The HTTP request resulting in the exception being raised.

required
exc VisitError

The exception being raised.

required

Returns:

Type Description
JSONAPIResponse

A JSON HTTP response through general_exception().

Source code in optimade/server/exception_handlers.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def grammar_not_implemented_handler(
    request: Request, exc: VisitError
) -> JSONAPIResponse:
    """Handle an error raised by Lark during filter transformation

    All errors raised during filter transformation are wrapped in the Lark `VisitError`.
    According to the OPTIMADE specification, these errors are repurposed to be 501 NotImplementedErrors.

    For special exceptions, like [`BadRequest`][optimade.exceptions.BadRequest], we pass-through to
    [`general_exception()`][optimade.server.exception_handlers.general_exception], since they should not
    return a 501 NotImplementedError.

    Parameters:
        request: The HTTP request resulting in the exception being raised.
        exc: The exception being raised.

    Returns:
        A JSON HTTP response through [`general_exception()`][optimade.server.exception_handlers.general_exception].

    """
    pass_through_exceptions = (BadRequest,)

    orig_exc = getattr(exc, "orig_exc", None)
    if isinstance(orig_exc, pass_through_exceptions):
        return general_exception(request, orig_exc)

    rule = getattr(exc.obj, "data", getattr(exc.obj, "type", str(exc)))

    status = 501
    title = "NotImplementedError"
    detail = (
        f"Error trying to process rule '{rule}'"
        if not str(exc.orig_exc)
        else str(exc.orig_exc)
    )
    error = OptimadeError(detail=detail, status=status, title=title)
    return general_exception(request, exc, status_code=status, errors=[error])

http_exception_handler(request, exc)

Handle a general HTTP Exception from Starlette

Parameters:

Name Type Description Default
request Request

The HTTP request resulting in the exception being raised.

required
exc Union[StarletteHTTPException, OptimadeHTTPException]

The exception being raised.

required

Returns:

Type Description
JSONAPIResponse

A JSON HTTP response through general_exception().

Source code in optimade/server/exception_handlers.py
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def http_exception_handler(
    request: Request,
    exc: Union[StarletteHTTPException, OptimadeHTTPException],
) -> JSONAPIResponse:
    """Handle a general HTTP Exception from Starlette

    Parameters:
        request: The HTTP request resulting in the exception being raised.
        exc: The exception being raised.

    Returns:
        A JSON HTTP response through [`general_exception()`][optimade.server.exception_handlers.general_exception].

    """
    return general_exception(request, exc)

meta_values(url, data_returned, data_available, more_data_available, schema=None, **kwargs)

Helper to initialize the meta values

Source code in optimade/server/routers/utils.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def meta_values(
    url: Union[urllib.parse.ParseResult, urllib.parse.SplitResult, StarletteURL, str],
    data_returned: Optional[int],
    data_available: int,
    more_data_available: bool,
    schema: Optional[str] = None,
    **kwargs,
) -> ResponseMeta:
    """Helper to initialize the meta values"""
    from optimade.models import ResponseMetaQuery

    if isinstance(url, str):
        url = urllib.parse.urlparse(url)

    # To catch all (valid) variations of the version part of the URL, a regex is used
    if re.match(r"/v[0-9]+(\.[0-9]+){,2}/.*", url.path) is not None:
        url_path = re.sub(r"/v[0-9]+(\.[0-9]+){,2}/", "/", url.path)
    else:
        url_path = url.path

    if schema is None:
        schema = CONFIG.schema_url if not CONFIG.is_index else CONFIG.index_schema_url

    return ResponseMeta(
        query=ResponseMetaQuery(representation=f"{url_path}?{url.query}"),
        api_version=__api_version__,
        time_stamp=datetime.now(),
        data_returned=data_returned,
        more_data_available=more_data_available,
        provider=CONFIG.provider,
        data_available=data_available,
        implementation=CONFIG.implementation,
        schema=schema,
        **kwargs,
    )

not_implemented_handler(request, exc)

Handle a standard NotImplementedError Python exception

Parameters:

Name Type Description Default
request Request

The HTTP request resulting in the exception being raised.

required
exc NotImplementedError

The exception being raised.

required

Returns:

Type Description
JSONAPIResponse

A JSON HTTP response through general_exception().

Source code in optimade/server/exception_handlers.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def not_implemented_handler(
    request: Request, exc: NotImplementedError
) -> JSONAPIResponse:
    """Handle a standard NotImplementedError Python exception

    Parameters:
        request: The HTTP request resulting in the exception being raised.
        exc: The exception being raised.

    Returns:
        A JSON HTTP response through [`general_exception()`][optimade.server.exception_handlers.general_exception].

    """
    status = 501
    title = "NotImplementedError"
    detail = str(exc)
    error = OptimadeError(detail=detail, status=status, title=title)
    return general_exception(request, exc, status_code=status, errors=[error])

request_validation_exception_handler(request, exc)

Handle a request validation error from FastAPI

RequestValidationError is a specialization of a general pydantic ValidationError. Pass-through directly to general_exception().

Parameters:

Name Type Description Default
request Request

The HTTP request resulting in the exception being raised.

required
exc RequestValidationError

The exception being raised.

required

Returns:

Type Description
JSONAPIResponse

A JSON HTTP response through general_exception().

Source code in optimade/server/exception_handlers.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def request_validation_exception_handler(
    request: Request, exc: RequestValidationError
) -> JSONAPIResponse:
    """Handle a request validation error from FastAPI

    `RequestValidationError` is a specialization of a general pydantic `ValidationError`.
    Pass-through directly to [`general_exception()`][optimade.server.exception_handlers.general_exception].

    Parameters:
        request: The HTTP request resulting in the exception being raised.
        exc: The exception being raised.

    Returns:
        A JSON HTTP response through [`general_exception()`][optimade.server.exception_handlers.general_exception].

    """
    return general_exception(request, exc)

validation_exception_handler(request, exc)

Handle a general pydantic validation error

The pydantic ValidationError usually contains a list of errors, this function extracts them and wraps them in the OPTIMADE specific error resource.

Parameters:

Name Type Description Default
request Request

The HTTP request resulting in the exception being raised.

required
exc ValidationError

The exception being raised.

required

Returns:

Type Description
JSONAPIResponse

A JSON HTTP response through general_exception().

Source code in optimade/server/exception_handlers.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def validation_exception_handler(
    request: Request, exc: ValidationError
) -> JSONAPIResponse:
    """Handle a general pydantic validation error

    The pydantic `ValidationError` usually contains a list of errors,
    this function extracts them and wraps them in the OPTIMADE specific error resource.

    Parameters:
        request: The HTTP request resulting in the exception being raised.
        exc: The exception being raised.

    Returns:
        A JSON HTTP response through [`general_exception()`][optimade.server.exception_handlers.general_exception].

    """
    status = 500
    title = "ValidationError"
    errors = set()
    for error in exc.errors():
        pointer = "/" + "/".join([str(_) for _ in error["loc"]])
        source = ErrorSource(pointer=pointer)
        code = error["type"]
        detail = error["msg"]
        errors.add(
            OptimadeError(
                detail=detail, status=status, title=title, source=source, code=code
            )
        )
    return general_exception(request, exc, status_code=status, errors=list(errors))