feat(rpc): add ETag header with md5 hash content

The ETag header can be used for client-side caching.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2021-10-29 23:10:20 -07:00
parent b3b31394e8
commit 6d376fed15
No known key found for this signature in database
GPG key ID: F7E46DED420788F3
3 changed files with 33 additions and 6 deletions

View file

@ -1,8 +1,12 @@
import hashlib
from http import HTTPStatus from http import HTTPStatus
from typing import List, Optional from typing import List, Optional
from urllib.parse import unquote from urllib.parse import unquote
from fastapi import APIRouter, Query, Request import orjson
from fastapi import APIRouter, Query, Request, Response
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from aurweb.ratelimit import check_ratelimit from aurweb.ratelimit import check_ratelimit
@ -74,4 +78,21 @@ async def rpc(request: Request,
# Prepare list of arguments for input. If 'arg' was given, it'll # Prepare list of arguments for input. If 'arg' was given, it'll
# be a list with one element. # be a list with one element.
arguments = parse_args(request) arguments = parse_args(request)
return JSONResponse(rpc.handle(arguments)) data = rpc.handle(arguments)
# Serialize `data` into JSON in a sorted fashion. This way, our
# ETag header produced below will never end up changed.
output = orjson.dumps(data, option=orjson.OPT_SORT_KEYS)
# Produce an md5 hash based on `output`.
md5 = hashlib.md5()
md5.update(output)
etag = md5.hexdigest()
# Finally, return our JSONResponse with the ETag header.
# The ETag header expects quotes to surround any identifier.
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
return Response(output.decode(), headers={
"Content-Type": "application/json",
"ETag": f'"{etag}"'
})

View file

@ -99,8 +99,7 @@ class RPC:
data: Dict[str, Any]): data: Dict[str, Any]):
# Walk through all related PackageDependencies and produce # Walk through all related PackageDependencies and produce
# the appropriate dict entries. # the appropriate dict entries.
depends = package.package_dependencies for dep in package.package_dependencies:
for dep in depends:
if dep.DepTypeID in DEP_TYPES: if dep.DepTypeID in DEP_TYPES:
key = DEP_TYPES.get(dep.DepTypeID) key = DEP_TYPES.get(dep.DepTypeID)
@ -114,8 +113,7 @@ class RPC:
data: Dict[str, Any]): data: Dict[str, Any]):
# Walk through all related PackageRelations and produce # Walk through all related PackageRelations and produce
# the appropriate dict entries. # the appropriate dict entries.
relations = package.package_relations for rel in package.package_relations:
for rel in relations:
if rel.RelTypeID in REL_TYPES: if rel.RelTypeID in REL_TYPES:
key = REL_TYPES.get(rel.RelTypeID) key = REL_TYPES.get(rel.RelTypeID)

View file

@ -488,3 +488,11 @@ def test_rpc_ratelimit(getint: mock.MagicMock, pipeline: Pipeline):
# The new first request should be good. # The new first request should be good.
response = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big") response = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big")
assert response.status_code == int(HTTPStatus.OK) assert response.status_code == int(HTTPStatus.OK)
def test_rpc_etag():
response1 = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big")
response2 = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big")
assert response1.headers.get("ETag") is not None
assert response1.headers.get("ETag") != str()
assert response1.headers.get("ETag") == response2.headers.get("ETag")