From 6d376fed1576e036d8a4ceb687493e647f3d8c0d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 29 Oct 2021 23:10:20 -0700 Subject: [PATCH] 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 --- aurweb/routers/rpc.py | 25 +++++++++++++++++++++++-- aurweb/rpc.py | 6 ++---- test/test_rpc.py | 8 ++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index 0616326b..0c52404c 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -1,8 +1,12 @@ +import hashlib + from http import HTTPStatus from typing import List, Optional 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 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 # be a list with one element. 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}"' + }) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index e92f9c70..87700a2f 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -99,8 +99,7 @@ class RPC: data: Dict[str, Any]): # Walk through all related PackageDependencies and produce # the appropriate dict entries. - depends = package.package_dependencies - for dep in depends: + for dep in package.package_dependencies: if dep.DepTypeID in DEP_TYPES: key = DEP_TYPES.get(dep.DepTypeID) @@ -114,8 +113,7 @@ class RPC: data: Dict[str, Any]): # Walk through all related PackageRelations and produce # the appropriate dict entries. - relations = package.package_relations - for rel in relations: + for rel in package.package_relations: if rel.RelTypeID in REL_TYPES: key = REL_TYPES.get(rel.RelTypeID) diff --git a/test/test_rpc.py b/test/test_rpc.py index 9400ee06..00703c23 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -488,3 +488,11 @@ def test_rpc_ratelimit(getint: mock.MagicMock, pipeline: Pipeline): # The new first request should be good. response = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big") 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")