diff --git a/docs/source/differences-to-vws.rst b/docs/source/differences-to-vws.rst index 7bb8390e0..949be5f27 100644 --- a/docs/source/differences-to-vws.rst +++ b/docs/source/differences-to-vws.rst @@ -374,7 +374,7 @@ path which does not start with a served path, such as For any other request which it does not serve, such as ``DELETE /summary`` or ``GET /targetsfoo``, it gives an HTML "Not Found" page which names the method and the path of the request. -The Flask and Docker mock gives an empty body for all of these. +The Flask and Docker mock reproduces both response shapes. The ``requests`` and ``httpx`` backends mock only the paths which the mock serves, so a request to any other path raises a connection error rather than diff --git a/src/mock_vws/_flask_server/vws.py b/src/mock_vws/_flask_server/vws.py index 3696f48d3..babb38aeb 100644 --- a/src/mock_vws/_flask_server/vws.py +++ b/src/mock_vws/_flask_server/vws.py @@ -6,6 +6,8 @@ import base64 import email.utils +import gzip +import html import json import logging import threading @@ -401,14 +403,39 @@ def handle_exceptions(exc: ValidatorError) -> Response: @VWS_FLASK_APP.errorhandler(code_or_exception=HTTPStatus.METHOD_NOT_ALLOWED) @beartype def handle_unrouted_request(exc: NotFound | MethodNotAllowed) -> Response: - """Return a 404 response with no body for a request which no route - serves. + """Return the real Vuforia 404 shape for an unrouted request. - Real Vuforia returns a 404 response for a request to a path which it does - not serve, and for a request to a served path with a method which that - path does not serve. + Vuforia's edge returns an empty response for unknown path prefixes. Paths + routed to the Cloud Targets application return its HTML Not Found page. """ del exc + application_prefixes = ("/duplicates", "/summary", "/targets") + if request.path.startswith(application_prefixes): + request_description = html.escape( + s=f"{request.method} {request.path}", + quote=True, + ) + body = f""" + + + Not Found + + +

Not Found

+

For request '{request_description}'

+ + +""".encode() + return Response( + status=HTTPStatus.NOT_FOUND, + response=gzip.compress(data=body), + headers={ + "Content-Type": "text/html; charset=UTF-8", + "Content-Encoding": "gzip", + "x-envoy-upstream-service-time": "5", + }, + ) + response = Response(status=HTTPStatus.NOT_FOUND, response=b"") del response.headers["Content-Type"] return response diff --git a/tests/mock_vws/test_invalid_given_id.py b/tests/mock_vws/test_invalid_given_id.py index 29eaf70ee..6602cd5f6 100644 --- a/tests/mock_vws/test_invalid_given_id.py +++ b/tests/mock_vws/test_invalid_given_id.py @@ -10,6 +10,7 @@ there are entries. """ +import gzip from dataclasses import dataclass from http import HTTPMethod, HTTPStatus @@ -38,6 +39,8 @@ class _UnroutedResponse: status_code: int body: bytes content_type: str | None + content_encoding: str | None + upstream_service_time: str | None @beartype @@ -77,12 +80,22 @@ def _send_unrouted_request( method=method, headers=headers, ) + response_body = test_client_response.data + content_encoding = test_client_response.headers.get( + key="Content-Encoding", + ) + if content_encoding == "gzip": + response_body = gzip.decompress(data=response_body) return _UnroutedResponse( status_code=test_client_response.status_code, - body=test_client_response.data, + body=response_body, content_type=test_client_response.headers.get( key="Content-Type", ), + content_encoding=content_encoding, + upstream_service_time=test_client_response.headers.get( + key="x-envoy-upstream-service-time", + ), ) try: @@ -99,6 +112,10 @@ def _send_unrouted_request( status_code=response.status_code, body=response.content, content_type=response.headers.get("Content-Type"), + content_encoding=response.headers.get("Content-Encoding"), + upstream_service_time=response.headers.get( + "x-envoy-upstream-service-time", + ), ) @@ -171,6 +188,8 @@ def test_unknown_path( assert response.status_code == HTTPStatus.NOT_FOUND assert response.body == b"" assert response.content_type is None + assert response.content_encoding is None + assert response.upstream_service_time is None @staticmethod def test_unknown_method( @@ -194,3 +213,8 @@ def test_unknown_method( assert response is not None assert response.status_code == HTTPStatus.NOT_FOUND + assert response.content_type == "text/html; charset=UTF-8" + assert response.content_encoding == "gzip" + assert response.upstream_service_time is not None + assert b"

Not Found

" in response.body + assert b"For request 'DELETE /summary'" in response.body