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
17 changes: 17 additions & 0 deletions matrix_common/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2021 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Any, Dict

JsonDict = Dict[str, Any]
151 changes: 151 additions & 0 deletions matrix_common/servlet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Copyright 2021 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import functools
import json
import logging
from typing import Any, Callable

from twisted.internet import defer
from twisted.python import failure
from twisted.web import server
from twisted.web.server import Request

from matrix_common.json import JsonDict

logger = logging.getLogger(__name__)


class MatrixRestError(Exception):
"""
Handled by the jsonwrap wrapper. Any servlets that don't use this
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does jsonwrap refer to the decorators below?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err, yes, I forgot to rename it here.

wrapper should catch this exception themselves.
"""

def __init__(self, httpStatus: int, errcode: str, error: str):
super(Exception, self).__init__(error)
self.httpStatus = httpStatus
self.errcode = errcode
self.error = error


def json_servlet_sync(f: Callable[..., JsonDict]) -> Callable[..., bytes]:
@functools.wraps(f)
def inner(self: Any, request: Request, *args: Any, **kwargs: Any) -> bytes:
"""
Runs a web handler function with the given request and parameters, then
converts its result into JSON and returns it. If an error happens, also sets
the HTTP response code.

Args:
self: The current object.
request: The request to process.
args: The arguments to pass to the function.
kwargs: The keyword arguments to pass to the function.

Returns:
The JSON payload to send as a response to the request.
"""
try:
request.setHeader("Content-Type", "application/json")
return dict_to_json_bytes(f(self, request, *args, **kwargs))
except MatrixRestError as e:
request.setResponseCode(e.httpStatus)
return dict_to_json_bytes({"errcode": e.errcode, "error": e.error})
except Exception:
logger.exception("Exception processing request")
request.setHeader("Content-Type", "application/json")
request.setResponseCode(500)
return dict_to_json_bytes(
{
"errcode": "M_UNKNOWN",
"error": "Internal Server Error",
}
)

return inner


def json_servlet_async(fn: Callable[..., JsonDict]) -> Callable[..., int]:
async def render(
fn: Callable[..., Any], self: Any, request: Request, **kwargs: Any
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We await and json encode the return from fn below, so we probably want it to return Awaitable[JsonDict]?

Suggested change
fn: Callable[..., Any], self: Any, request: Request, **kwargs: Any
fn: Callable[..., Awaitable[JsonDict]], self: Any, request: Request, **kwargs: Any

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

) -> None:
"""
Runs an asynchronous web handler function with the given request and parameters,
then converts its result into JSON bytes and writes it to the request. If an error
happens, also sets the HTTP response code.

Args:
fn: The handler to run.
self: The current object.
request: The request to process.
args: The arguments to pass to the function.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the *args argument missing?

kwargs: The keyword arguments to pass to the function.
"""

request.setHeader("Content-Type", "application/json")
try:
result = await fn(self, request, **kwargs)
request.write(dict_to_json_bytes(result))
except MatrixRestError as e:
request.setResponseCode(e.httpStatus)
request.write(dict_to_json_bytes({"errcode": e.errcode, "error": e.error}))
except Exception:
f = failure.Failure()
logger.error("Request processing failed: %r, %s", f, f.getTraceback())
request.setResponseCode(500)
request.write(
dict_to_json_bytes(
{"errcode": "M_UNKNOWN", "error": "Internal Server Error"}
)
)
request.finish()

@functools.wraps(fn)
def inner(*args: Any, **kwargs: Any) -> int:
"""
Runs an asynchronous web handler function with the given arguments.

Args:
args: The arguments to pass to the function.
kwargs: The keyword arguments to pass to the function.

Returns:
A special code to tell the servlet that the response isn't ready yet
and will come later.
"""
defer.ensureDeferred(render(fn, *args, **kwargs))
return server.NOT_DONE_YET

return inner


def send_cors(request: Request) -> None:
"""Send CORS headers when handling a request."""
request.setHeader("Access-Control-Allow-Origin", "*")
request.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
request.setHeader("Access-Control-Allow-Headers", "*")


def dict_to_json_bytes(content: JsonDict) -> bytes:
"""
Converts a dict into JSON and encodes it to bytes.

Args:
content: The dict to convert.

Returns:
The JSON bytes.
"""
return json.dumps(content).encode("UTF-8")
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[mypy]
strict = true
# Allow calling untyped functions in typed contexts. This is needed because we sometimes
# call methods and functions from Twisted, which aren't typed.
allow_untyped_calls = true
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ packages =
python_requires = >= 3.6
install_requires =
attrs
twisted


[options.extras_require]
dev =
# for tests
tox
twisted
aiounittest
# for type checking
mypy == 0.910
Expand Down