diff --git a/openapi/index_openapi.json b/openapi/index_openapi.json index 9fc7609b0..7a2542cfb 100644 --- a/openapi/index_openapi.json +++ b/openapi/index_openapi.json @@ -17,7 +17,7 @@ "200": { "description": "Successful Response", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/IndexInfoResponse" } @@ -27,7 +27,7 @@ "400": { "description": "Bad Request", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -37,7 +37,7 @@ "403": { "description": "Forbidden", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -47,7 +47,7 @@ "404": { "description": "Not Found", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -57,7 +57,7 @@ "422": { "description": "Unprocessable Entity", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -67,7 +67,7 @@ "500": { "description": "Internal Server Error", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -77,7 +77,7 @@ "501": { "description": "Not Implemented", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -87,7 +87,7 @@ "553": { "description": "Version Not Supported", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -276,7 +276,7 @@ "200": { "description": "Successful Response", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/LinksResponse" } @@ -286,7 +286,7 @@ "400": { "description": "Bad Request", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -296,7 +296,7 @@ "403": { "description": "Forbidden", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -306,7 +306,7 @@ "404": { "description": "Not Found", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -316,7 +316,7 @@ "422": { "description": "Unprocessable Entity", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -326,7 +326,7 @@ "500": { "description": "Internal Server Error", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -336,7 +336,7 @@ "501": { "description": "Not Implemented", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -346,7 +346,7 @@ "553": { "description": "Version Not Supported", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } diff --git a/openapi/openapi.json b/openapi/openapi.json index 6c9c7bf79..7c95801cf 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -17,7 +17,7 @@ "200": { "description": "Successful Response", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/InfoResponse" } @@ -27,7 +27,7 @@ "400": { "description": "Bad Request", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -37,7 +37,7 @@ "403": { "description": "Forbidden", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -47,7 +47,7 @@ "404": { "description": "Not Found", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -57,7 +57,7 @@ "422": { "description": "Unprocessable Entity", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -67,7 +67,7 @@ "500": { "description": "Internal Server Error", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -77,7 +77,7 @@ "501": { "description": "Not Implemented", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -87,7 +87,7 @@ "553": { "description": "Version Not Supported", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -119,7 +119,7 @@ "200": { "description": "Successful Response", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/EntryInfoResponse" } @@ -129,7 +129,7 @@ "400": { "description": "Bad Request", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -139,7 +139,7 @@ "403": { "description": "Forbidden", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -149,7 +149,7 @@ "404": { "description": "Not Found", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -159,7 +159,7 @@ "422": { "description": "Unprocessable Entity", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -169,7 +169,7 @@ "500": { "description": "Internal Server Error", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -179,7 +179,7 @@ "501": { "description": "Not Implemented", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -189,7 +189,7 @@ "553": { "description": "Version Not Supported", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -378,7 +378,7 @@ "200": { "description": "Successful Response", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/LinksResponse" } @@ -388,7 +388,7 @@ "400": { "description": "Bad Request", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -398,7 +398,7 @@ "403": { "description": "Forbidden", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -408,7 +408,7 @@ "404": { "description": "Not Found", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -418,7 +418,7 @@ "422": { "description": "Unprocessable Entity", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -428,7 +428,7 @@ "500": { "description": "Internal Server Error", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -438,7 +438,7 @@ "501": { "description": "Not Implemented", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -448,7 +448,7 @@ "553": { "description": "Version Not Supported", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -637,7 +637,7 @@ "200": { "description": "Successful Response", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ReferenceResponseMany" } @@ -647,7 +647,7 @@ "400": { "description": "Bad Request", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -657,7 +657,7 @@ "403": { "description": "Forbidden", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -667,7 +667,7 @@ "404": { "description": "Not Found", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -677,7 +677,7 @@ "422": { "description": "Unprocessable Entity", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -687,7 +687,7 @@ "500": { "description": "Internal Server Error", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -697,7 +697,7 @@ "501": { "description": "Not Implemented", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -707,7 +707,7 @@ "553": { "description": "Version Not Supported", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -802,7 +802,7 @@ "200": { "description": "Successful Response", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ReferenceResponseOne" } @@ -812,7 +812,7 @@ "400": { "description": "Bad Request", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -822,7 +822,7 @@ "403": { "description": "Forbidden", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -832,7 +832,7 @@ "404": { "description": "Not Found", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -842,7 +842,7 @@ "422": { "description": "Unprocessable Entity", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -852,7 +852,7 @@ "500": { "description": "Internal Server Error", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -862,7 +862,7 @@ "501": { "description": "Not Implemented", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -872,7 +872,7 @@ "553": { "description": "Version Not Supported", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1061,7 +1061,7 @@ "200": { "description": "Successful Response", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/StructureResponseMany" } @@ -1071,7 +1071,7 @@ "400": { "description": "Bad Request", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1081,7 +1081,7 @@ "403": { "description": "Forbidden", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1091,7 +1091,7 @@ "404": { "description": "Not Found", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1101,7 +1101,7 @@ "422": { "description": "Unprocessable Entity", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1111,7 +1111,7 @@ "500": { "description": "Internal Server Error", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1121,7 +1121,7 @@ "501": { "description": "Not Implemented", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1131,7 +1131,7 @@ "553": { "description": "Version Not Supported", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1226,7 +1226,7 @@ "200": { "description": "Successful Response", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/StructureResponseOne" } @@ -1236,7 +1236,7 @@ "400": { "description": "Bad Request", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1246,7 +1246,7 @@ "403": { "description": "Forbidden", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1256,7 +1256,7 @@ "404": { "description": "Not Found", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1266,7 +1266,7 @@ "422": { "description": "Unprocessable Entity", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1276,7 +1276,7 @@ "500": { "description": "Internal Server Error", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1286,7 +1286,7 @@ "501": { "description": "Not Implemented", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -1296,7 +1296,7 @@ "553": { "description": "Version Not Supported", "content": { - "application/json": { + "application/vnd.api+json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } diff --git a/optimade/server/exception_handlers.py b/optimade/server/exception_handlers.py index 75ce9e098..4510767a2 100644 --- a/optimade/server/exception_handlers.py +++ b/optimade/server/exception_handlers.py @@ -7,14 +7,13 @@ from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError, StarletteHTTPException from fastapi import Request -from fastapi.responses import JSONResponse from optimade.models import OptimadeError, ErrorResponse, ErrorSource from optimade.server.config import CONFIG from optimade.server.exceptions import BadRequest from optimade.server.logger import LOGGER -from optimade.server.routers.utils import meta_values +from optimade.server.routers.utils import meta_values, JSONAPIResponse def general_exception( @@ -22,7 +21,7 @@ def general_exception( exc: Exception, status_code: int = 500, # A status_code in `exc` will take precedence errors: List[OptimadeError] = None, -) -> JSONResponse: +) -> JSONAPIResponse: """Handle an exception Parameters: @@ -73,7 +72,7 @@ def general_exception( errors=errors, ) - return JSONResponse( + return JSONAPIResponse( status_code=http_response_code, content=jsonable_encoder(response, exclude_unset=True), ) @@ -81,7 +80,7 @@ def general_exception( def http_exception_handler( request: Request, exc: StarletteHTTPException -) -> JSONResponse: +) -> JSONAPIResponse: """Handle a general HTTP Exception from Starlette Parameters: @@ -97,7 +96,7 @@ def http_exception_handler( def request_validation_exception_handler( request: Request, exc: RequestValidationError -) -> JSONResponse: +) -> JSONAPIResponse: """Handle a request validation error from FastAPI `RequestValidationError` is a specialization of a general pydantic `ValidationError`. @@ -116,7 +115,7 @@ def request_validation_exception_handler( def validation_exception_handler( request: Request, exc: ValidationError -) -> JSONResponse: +) -> JSONAPIResponse: """Handle a general pydantic validation error The pydantic `ValidationError` usually contains a list of errors, @@ -146,7 +145,9 @@ def validation_exception_handler( return general_exception(request, exc, status_code=status, errors=list(errors)) -def grammar_not_implemented_handler(request: Request, exc: VisitError) -> JSONResponse: +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`. @@ -183,7 +184,9 @@ def grammar_not_implemented_handler(request: Request, exc: VisitError) -> JSONRe return general_exception(request, exc, status_code=status, errors=[error]) -def not_implemented_handler(request: Request, exc: NotImplementedError) -> JSONResponse: +def not_implemented_handler( + request: Request, exc: NotImplementedError +) -> JSONAPIResponse: """Handle a standard NotImplementedError Python exception Parameters: @@ -201,7 +204,7 @@ def not_implemented_handler(request: Request, exc: NotImplementedError) -> JSONR return general_exception(request, exc, status_code=status, errors=[error]) -def general_exception_handler(request: Request, exc: Exception) -> JSONResponse: +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]. @@ -217,7 +220,9 @@ def general_exception_handler(request: Request, exc: Exception) -> JSONResponse: return general_exception(request, exc) -OPTIMADE_EXCEPTIONS: Tuple[Exception, Callable[[Request, Exception], JSONResponse]] = ( +OPTIMADE_EXCEPTIONS: Tuple[ + Exception, Callable[[Request, Exception], JSONAPIResponse] +] = ( (StarletteHTTPException, http_exception_handler), (RequestValidationError, request_validation_exception_handler), (ValidationError, validation_exception_handler), diff --git a/optimade/server/exceptions.py b/optimade/server/exceptions.py index f58485e97..c4daf9eaf 100644 --- a/optimade/server/exceptions.py +++ b/optimade/server/exceptions.py @@ -1,4 +1,5 @@ -from fastapi import HTTPException +from abc import ABC +from fastapi import HTTPException as FastAPIHTTPException __all__ = ( "BadRequest", @@ -11,113 +12,83 @@ ) -class StrReprMixin(HTTPException): - """This mixin can be useful when testing requires a string - representation of an exception that contains the HTTPException +class HTTPException(FastAPIHTTPException, ABC): + """This abstract class makes it easier to subclass FastAPI's HTTPException with + new status codes. + + It can also be useful when testing requires a string representation + of an exception that contains the HTTPException detail string, rather than the standard Python exception message. + + Attributes: + status_code: The HTTP status code accompanying this exception. + title: A descriptive title for this exception. + """ - def __str__(self): + status_code: int = None + title: str + + def __init__(self, detail: str = None, headers: dict = None) -> None: + if self.status_code is None: + raise AttributeError( + "HTTPException class {self.__class__.__name__} is missing required `status_code` attribute." + ) + + super().__init__(status_code=self.status_code, detail=detail, headers=headers) + + def __str__(self) -> str: return self.detail if self.detail is not None else self.__repr__() -class BadRequest(StrReprMixin, HTTPException): +class BadRequest(HTTPException): """400 Bad Request""" status_code: int = 400 title: str = "Bad Request" - def __init__( - self, - detail: str = None, - headers: dict = None, - ) -> None: - super().__init__(status_code=self.status_code, detail=detail, headers=headers) - -class VersionNotSupported(StrReprMixin, HTTPException): +class VersionNotSupported(HTTPException): """553 Version Not Supported""" status_code: int = 553 title: str = "Version Not Supported" - def __init__( - self, - detail: str = None, - headers: dict = None, - ) -> None: - super().__init__(status_code=self.status_code, detail=detail, headers=headers) - -class Forbidden(StrReprMixin, HTTPException): +class Forbidden(HTTPException): """403 Forbidden""" status_code: int = 403 title: str = "Forbidden" - def __init__( - self, - detail: str = None, - headers: dict = None, - ) -> None: - super().__init__(status_code=self.status_code, detail=detail, headers=headers) - -class NotFound(StrReprMixin, HTTPException): +class NotFound(HTTPException): """404 Not Found""" status_code: int = 404 title: str = "Not Found" - def __init__( - self, - detail: str = None, - headers: dict = None, - ) -> None: - super().__init__(status_code=self.status_code, detail=detail, headers=headers) - -class UnprocessableEntity(StrReprMixin, HTTPException): +class UnprocessableEntity(HTTPException): """422 Unprocessable Entity""" status_code: int = 422 title: str = "Unprocessable Entity" - def __init__( - self, - detail: str = None, - headers: dict = None, - ) -> None: - super().__init__(status_code=self.status_code, detail=detail, headers=headers) - -class InternalServerError(StrReprMixin, HTTPException): +class InternalServerError(HTTPException): """500 Internal Server Error""" status_code: int = 500 title: str = "Internal Server Error" - def __init__( - self, - detail: str = None, - headers: dict = None, - ) -> None: - super().__init__(status_code=self.status_code, detail=detail, headers=headers) - -class NotImplementedResponse(StrReprMixin, HTTPException): +class NotImplementedResponse(HTTPException): """501 Not Implemented""" status_code: int = 501 title: str = "Not Implemented" - def __init__( - self, - detail: str = None, - headers: dict = None, - ) -> None: - super().__init__(status_code=self.status_code, detail=detail, headers=headers) - POSSIBLE_ERRORS = ( BadRequest, diff --git a/optimade/server/main.py b/optimade/server/main.py index c17248f86..7b1b708d2 100644 --- a/optimade/server/main.py +++ b/optimade/server/main.py @@ -30,7 +30,7 @@ structures, versions, ) -from optimade.server.routers.utils import BASE_URL_PREFIXES +from optimade.server.routers.utils import BASE_URL_PREFIXES, JSONAPIResponse if os.getenv("OPTIMADE_CONFIG_FILE") is None: @@ -56,6 +56,7 @@ docs_url=f"{BASE_URL_PREFIXES['major']}/extensions/docs", redoc_url=f"{BASE_URL_PREFIXES['major']}/extensions/redoc", openapi_url=f"{BASE_URL_PREFIXES['major']}/extensions/openapi.json", + default_response_class=JSONAPIResponse, ) diff --git a/optimade/server/main_index.py b/optimade/server/main_index.py index ac9aeea95..2985c6ea6 100644 --- a/optimade/server/main_index.py +++ b/optimade/server/main_index.py @@ -22,7 +22,7 @@ from optimade.server.exception_handlers import OPTIMADE_EXCEPTIONS from optimade.server.middleware import OPTIMADE_MIDDLEWARE from optimade.server.routers import index_info, links, versions -from optimade.server.routers.utils import BASE_URL_PREFIXES +from optimade.server.routers.utils import BASE_URL_PREFIXES, JSONAPIResponse if os.getenv("OPTIMADE_CONFIG_FILE") is None: LOGGER.warn( @@ -49,6 +49,7 @@ docs_url=f"{BASE_URL_PREFIXES['major']}/extensions/docs", redoc_url=f"{BASE_URL_PREFIXES['major']}/extensions/redoc", openapi_url=f"{BASE_URL_PREFIXES['major']}/extensions/openapi.json", + default_response_class=JSONAPIResponse, ) diff --git a/optimade/server/routers/utils.py b/optimade/server/routers/utils.py index 4c2de802c..d55938e73 100644 --- a/optimade/server/routers/utils.py +++ b/optimade/server/routers/utils.py @@ -4,7 +4,8 @@ from datetime import datetime from typing import Any, Dict, List, Set, Union -from fastapi import HTTPException, Request +from fastapi import Request +from fastapi.responses import JSONResponse from starlette.datastructures import URL as StarletteURL from optimade import __api_version__ @@ -18,7 +19,7 @@ from optimade.server.config import CONFIG from optimade.server.entry_collections import EntryCollection -from optimade.server.exceptions import BadRequest +from optimade.server.exceptions import BadRequest, InternalServerError from optimade.server.query_params import EntryListingQueryParams, SingleEntryQueryParams # we need to get rid of any release tags (e.g. -rc.2) and any build metadata (e.g. +py36) @@ -30,6 +31,15 @@ } +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" + + def meta_values( url: Union[urllib.parse.ParseResult, urllib.parse.SplitResult, StarletteURL, str], data_returned: int, @@ -275,8 +285,7 @@ def get_single_entry( included = get_included_relationships(results, ENTRY_COLLECTIONS, include) if more_data_available: - raise HTTPException( - status_code=500, + raise InternalServerError( detail=f"more_data_available MUST be False for single entry response, however it is {more_data_available}", ) diff --git a/tests/server/conftest.py b/tests/server/conftest.py index 5353f2989..0f780ddb6 100644 --- a/tests/server/conftest.py +++ b/tests/server/conftest.py @@ -75,6 +75,10 @@ def inner( response = used_client.get(request, **kwargs) response_json = response.json() assert response.status_code == 200, f"Request failed: {response_json}" + expected_mime_type = "application/vnd.api+json" + assert ( + response.headers["content-type"] == expected_mime_type + ), f"Response should have MIME type {expected_mime_type!r}, not {response.headers['content-type']!r}." except json.JSONDecodeError: print( f"Request attempted:\n{used_client.base_url}{used_client.version}" @@ -174,6 +178,10 @@ def inner( f"Request should have been an error with status code {expected_status}, " f"but instead {response.status_code} was received.\nResponse:\n{response.json()}", ) + expected_mime_type = "application/vnd.api+json" + assert ( + response.headers["content-type"] == expected_mime_type + ), f"Response should have MIME type {expected_mime_type!r}, not {response.headers['content-type']!r}." response = response.json() assert len(response["errors"]) == 1, response.get(