Skip to content

Commit

Permalink
replaced-http-statuses-with-HTTPStatus-from-http-stdlib (#1174)
Browse files Browse the repository at this point in the history
  • Loading branch information
DamianCzajkowski authored Apr 5, 2024
1 parent 2bde665 commit e3d8e6b
Show file tree
Hide file tree
Showing 20 changed files with 169 additions and 127 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.24 (UNRELEASED)

- Added validation for directive declarations in `make_executable_schema` to prevent schema creation with undeclared directives.
- Replaced hardcoded HTTP statuses with `HTTPStatus` from the `http` stdlib module.


## 0.23 (2024-03-18)
Expand Down
15 changes: 8 additions & 7 deletions ariadne/asgi/handlers/http.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from http import HTTPStatus
from inspect import isawaitable
from typing import Any, Optional, Type, Union, cast

Expand All @@ -8,12 +9,12 @@
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse, Response
from starlette.types import Receive, Scope, Send

from ...explorer import Explorer
from ...constants import (
DATA_TYPE_JSON,
DATA_TYPE_MULTIPART,
)
from ...exceptions import HttpBadRequestError, HttpError
from ...explorer import Explorer
from ...file_uploads import combine_multipart_data
from ...graphql import graphql
from ...types import (
Expand Down Expand Up @@ -164,7 +165,9 @@ async def graphql_http_server(self, request: Request) -> Response:
try:
data = await self.extract_data_from_request(request)
except HttpError as error:
return PlainTextResponse(error.message or error.status, status_code=400)
return PlainTextResponse(
error.message or error.status, status_code=HTTPStatus.BAD_REQUEST
)

success, result = await self.execute_graphql_query(request, data)
return await self.create_json_response(request, result, success)
Expand Down Expand Up @@ -193,9 +196,7 @@ async def extract_data_from_request(self, request: Request):
return self.extract_data_from_get_request(request)

raise HttpBadRequestError(
"Posted content must be of type {} or {}".format(
DATA_TYPE_JSON, DATA_TYPE_MULTIPART
)
f"Posted content must be of type {DATA_TYPE_JSON} or {DATA_TYPE_MULTIPART}"
)

async def extract_data_from_json_request(self, request: Request) -> dict:
Expand Down Expand Up @@ -402,7 +403,7 @@ async def create_json_response(
`success`: a `bool` specifying if
"""
status_code = 200 if success else 400
status_code = HTTPStatus.OK if success else HTTPStatus.BAD_REQUEST
return JSONResponse(result, status_code=status_code)

def handle_not_allowed_method(self, request: Request):
Expand All @@ -423,4 +424,4 @@ def handle_not_allowed_method(self, request: Request):
if request.method == "OPTIONS":
return Response(headers=allow_header)

return Response(status_code=405, headers=allow_header)
return Response(status_code=HTTPStatus.METHOD_NOT_ALLOWED, headers=allow_header)
11 changes: 8 additions & 3 deletions ariadne/constants.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from enum import Enum
from http import HTTPStatus

DATA_TYPE_JSON = "application/json"
DATA_TYPE_MULTIPART = "multipart/form-data"

CONTENT_TYPE_JSON = "application/json; charset=UTF-8"
CONTENT_TYPE_TEXT_HTML = "text/html; charset=UTF-8"
CONTENT_TYPE_TEXT_PLAIN = "text/plain; charset=UTF-8"

HTTP_STATUS_200_OK = "200 OK"
HTTP_STATUS_400_BAD_REQUEST = "400 Bad Request"
HTTP_STATUS_405_METHOD_NOT_ALLOWED = "405 Method Not Allowed"

class HttpStatusResponse(Enum):
OK = f"{HTTPStatus.OK} OK"
BAD_REQUEST = f"{HTTPStatus.BAD_REQUEST} BAD REQUEST"
METHOD_NOT_ALLOWED = f"{HTTPStatus.METHOD_NOT_ALLOWED} METHOD NOT ALLOWED"
19 changes: 10 additions & 9 deletions ariadne/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
import os
from typing import Optional, Union

from .constants import HTTP_STATUS_400_BAD_REQUEST
from .constants import HttpStatusResponse


class HttpError(Exception):
"""Base class for HTTP errors raised inside the ASGI and WSGI servers."""

status = ""
def __init__(self, status: str, message: Optional[str] = None) -> None:
"""Initializes the `HttpError` with a status and optional error message.
def __init__(self, message: Optional[str] = None) -> None:
"""Initializes the `HttpError` with optional error message.
# Optional arguments
# Arguments
`message`: a `str` with error message to return in response body or
`None`.
`status`: HTTP status code as `HttpStatusResponse`.
`message`: Optional error message to return in the response body.
"""
super().__init__()
self.status = status
self.message = message


class HttpBadRequestError(HttpError):
"""Raised when request did not contain the data required to execute
the GraphQL query."""

status = HTTP_STATUS_400_BAD_REQUEST
def __init__(self, message: Optional[str] = None) -> None:
"""Initializes the `HttpBadRequestError` with optional error message."""
super().__init__(HttpStatusResponse.BAD_REQUEST.value, message)


class GraphQLFileSyntaxError(Exception):
Expand Down
24 changes: 13 additions & 11 deletions ariadne/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
CONTENT_TYPE_TEXT_PLAIN,
DATA_TYPE_JSON,
DATA_TYPE_MULTIPART,
HTTP_STATUS_200_OK,
HTTP_STATUS_400_BAD_REQUEST,
HTTP_STATUS_405_METHOD_NOT_ALLOWED,
HttpStatusResponse,
)
from .exceptions import HttpBadRequestError, HttpError
from .explorer import Explorer, ExplorerGraphiQL
Expand Down Expand Up @@ -206,7 +204,7 @@ def handle_graphql_error(
`start_response`: a callable used to begin new HTTP response.
"""
start_response(
HTTP_STATUS_400_BAD_REQUEST, [("Content-Type", CONTENT_TYPE_JSON)]
HttpStatusResponse.BAD_REQUEST.value, [("Content-Type", CONTENT_TYPE_JSON)]
)
error_json = {"errors": [{"message": error.message}]}
return [json.dumps(error_json).encode("utf-8")]
Expand Down Expand Up @@ -319,7 +317,9 @@ def handle_get_explorer(self, environ: dict, start_response) -> List[bytes]:
if not explorer_html:
return self.handle_not_allowed_method(environ, start_response)

start_response(HTTP_STATUS_200_OK, [("Content-Type", CONTENT_TYPE_TEXT_HTML)])
start_response(
HttpStatusResponse.OK.value, [("Content-Type", CONTENT_TYPE_TEXT_HTML)]
)
return [cast(str, explorer_html).encode("utf-8")]

def handle_post(self, environ: dict, start_response: Callable) -> List[bytes]:
Expand Down Expand Up @@ -355,9 +355,7 @@ def get_request_data(self, environ: dict) -> dict:
return self.extract_data_from_multipart_request(environ)

raise HttpBadRequestError(
"Posted content must be of type {} or {}".format(
DATA_TYPE_JSON, DATA_TYPE_MULTIPART
)
f"Posted content must be of type {DATA_TYPE_JSON} or {DATA_TYPE_MULTIPART}"
)

def extract_data_from_json_request(self, environ: dict) -> Any:
Expand Down Expand Up @@ -558,7 +556,11 @@ def return_response_from_result(
`result`: a `GraphQLResult` for this request.
"""
success, response = result
status_str = HTTP_STATUS_200_OK if success else HTTP_STATUS_400_BAD_REQUEST
status_str = (
HttpStatusResponse.OK.value
if success
else HttpStatusResponse.BAD_REQUEST.value
)
start_response(status_str, [("Content-Type", CONTENT_TYPE_JSON)])
return [json.dumps(response).encode("utf-8")]

Expand All @@ -581,9 +583,9 @@ def handle_not_allowed_method(
allowed_methods.append("GET")

if environ["REQUEST_METHOD"] == "OPTIONS":
status_str = HTTP_STATUS_200_OK
status_str = HttpStatusResponse.OK.value
else:
status_str = HTTP_STATUS_405_METHOD_NOT_ALLOWED
status_str = HttpStatusResponse.METHOD_NOT_ALLOWED.value

headers = [
("Content-Type", CONTENT_TYPE_TEXT_PLAIN),
Expand Down
10 changes: 6 additions & 4 deletions benchmark/test_extensions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from http import HTTPStatus

from starlette.testclient import TestClient

from ariadne.asgi import GraphQL
Expand All @@ -23,7 +25,7 @@ def api_call():
)

result = benchmark(api_call)
assert result.status_code == 200
assert result.status_code == HTTPStatus.OK
assert not result.json().get("errors")


Expand All @@ -45,7 +47,7 @@ def api_call():
)

result = benchmark(api_call)
assert result.status_code == 200
assert result.status_code == HTTPStatus.OK
assert not result.json().get("errors")


Expand All @@ -67,7 +69,7 @@ def api_call():
)

result = benchmark(api_call)
assert result.status_code == 200
assert result.status_code == HTTPStatus.OK
assert not result.json().get("errors")


Expand All @@ -89,5 +91,5 @@ def api_call():
)

result = benchmark(api_call)
assert result.status_code == 200
assert result.status_code == HTTPStatus.OK
assert not result.json().get("errors")
5 changes: 3 additions & 2 deletions tests/asgi/test_configuration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# pylint: disable=not-context-manager
import time
from datetime import timedelta
from http import HTTPStatus
from unittest.mock import ANY, Mock

import pytest
Expand Down Expand Up @@ -391,15 +392,15 @@ def test_query_over_get_fails_if_variables_are_not_json_serialized(schema):
"&operationName=Hello"
'&variables={"name" "John"}'
)
assert response.status_code == 400
assert response.status_code == HTTPStatus.BAD_REQUEST
assert response.content == b"Variables query arg is not a valid JSON"


def test_query_over_get_is_not_executed_if_not_enabled(schema):
app = GraphQL(schema, execute_get_queries=False)
client = TestClient(app)
response = client.get("/?query={ status }")
assert response.status_code == 200
assert response.status_code == HTTPStatus.OK
assert response.headers["CONTENT-TYPE"] == "text/html; charset=utf-8"


Expand Down
12 changes: 7 additions & 5 deletions tests/asgi/test_explorer.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,45 @@
from http import HTTPStatus

from starlette.testclient import TestClient

from ariadne.asgi import GraphQL
from ariadne.explorer import (
ExplorerApollo,
ExplorerGraphiQL,
ExplorerHttp405,
ExplorerPlayground,
)
from ariadne.asgi import GraphQL


def test_default_explorer_html_is_served_on_get_request(schema, snapshot):
app = GraphQL(schema)
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200
assert response.status_code == HTTPStatus.OK
assert snapshot == response.text


def test_apollo_html_is_served_on_get_request(schema, snapshot):
app = GraphQL(schema, explorer=ExplorerApollo())
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200
assert response.status_code == HTTPStatus.OK
assert snapshot == response.text


def test_graphiql_html_is_served_on_get_request(schema, snapshot):
app = GraphQL(schema, explorer=ExplorerGraphiQL())
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200
assert response.status_code == HTTPStatus.OK
assert snapshot == response.text


def test_playground_html_is_served_on_get_request(schema, snapshot):
app = GraphQL(schema, explorer=ExplorerPlayground())
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200
assert response.status_code == HTTPStatus.OK
assert snapshot == response.text


Expand Down
6 changes: 4 additions & 2 deletions tests/asgi/test_http_methods.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from http import HTTPStatus

from starlette.testclient import TestClient

from ariadne.asgi import GraphQL


def test_options_method_is_supported(client):
response = client.options("/")
assert response.status_code == 200
assert response.status_code == HTTPStatus.OK
assert response.headers["Allow"] == "OPTIONS, POST, GET"


Expand All @@ -14,7 +16,7 @@ def test_options_response_excludes_get_if_introspection_is_disabled(schema):
client = TestClient(app)

response = client.options("/")
assert response.status_code == 200
assert response.status_code == HTTPStatus.OK
assert response.headers["Allow"] == "OPTIONS, POST"


Expand Down
Loading

0 comments on commit e3d8e6b

Please sign in to comment.