Skip to content

Commit a652e20

Browse files
adamtheturtleclaude
andcommitted
Reject an empty body on VWS endpoints which take JSON
An empty body given to POST /targets, PUT /targets/{id}, POST /targets/{id}/instances or the reco counts report endpoint raised an uncaught JSONDecodeError from validate_keys, because validate_json returned early for an empty body. Match real Vuforia instead: 500 Fail for the target endpoints, 400 Fail for the reco counts report and 400 BadRequest for instance generation. Add a test which runs against real Vuforia, since an empty body is answered promptly unlike other malformed bodies. Closes #3550 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 7f49489 commit a652e20

5 files changed

Lines changed: 111 additions & 7 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ jobs:
8989
- tests/mock_vws/test_invalid_json.py::TestInvalidJSON::test_not_json
9090
- tests/mock_vws/test_invalid_json.py::TestInvalidJSON::test_not_an_object
9191
- tests/mock_vws/test_invalid_json.py::TestInvalidJSON::test_not_utf_8
92+
- tests/mock_vws/test_invalid_json.py::TestInvalidJSON::test_empty_body
9293
- tests/mock_vws/test_invalid_json.py::TestInvalidJSON::test_invalid_json_with_skewed_time
9394
- tests/mock_vws/test_target_list.py
9495
- tests/mock_vws/test_reco_counts_report.py

newsfragments/3550.change

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Return a response, rather than raising an uncaught ``JSONDecodeError``, when an empty body is given to a VWS endpoint which takes a JSON body.
2+
As real Vuforia does, ``POST /targets`` and ``PUT /targets/<target_id>`` now return a 500 ``Fail`` response, the reco counts report endpoint returns a 400 ``Fail`` response, and the VuMark instance generation endpoint returns a 400 ``BadRequest`` response.

src/mock_vws/_services_validators/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ def run_services_validators[DatabaseT: AnyDatabase](
135135
validate_date_format(request_headers=request_headers)
136136
validate_date_in_range(request_headers=request_headers)
137137

138-
validate_json(request_body=request_body, request_path=request_path)
138+
validate_json(
139+
request_body=request_body,
140+
request_path=request_path,
141+
request_method=request_method,
142+
)
139143

140144
validate_keys(
141145
request_body=request_body,

src/mock_vws/_services_validators/json_validators.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,39 @@ def validate_body_given(*, request_body: bytes, request_method: str) -> None:
4444

4545

4646
@beartype
47-
def validate_json(*, request_body: bytes, request_path: str) -> None:
47+
def validate_json(
48+
*,
49+
request_body: bytes,
50+
request_path: str,
51+
request_method: str,
52+
) -> None:
4853
"""Validate that any given body is valid JSON.
4954
5055
Args:
5156
request_body: The body of the request.
5257
request_path: The path of the request.
58+
request_method: The HTTP method of the request.
5359
5460
Raises:
55-
BadRequestError: The request body is not valid UTF-8, or includes
56-
invalid JSON, for the VuMark instance generation endpoint.
57-
FailError: The request body is not valid UTF-8, or includes invalid
58-
JSON, for other endpoints.
61+
BadRequestError: The request body is empty, is not valid UTF-8, or
62+
includes invalid JSON, for the VuMark instance generation
63+
endpoint.
64+
FailError: The request body is empty, is not valid UTF-8, or includes
65+
invalid JSON, for other endpoints.
5966
"""
6067
if not request_body:
61-
return
68+
if request_method not in {HTTPMethod.POST, HTTPMethod.PUT}:
69+
return
70+
71+
_LOGGER.warning(msg="The request body is empty.")
72+
if request_path.endswith("/instances"):
73+
raise BadRequestError
74+
# Vuforia reports a server error for an empty body given to the
75+
# target endpoints, but a bad request for one given to the reco
76+
# counts report endpoint.
77+
if request_path.endswith("/reports/recoCounts"):
78+
raise FailError(status_code=HTTPStatus.BAD_REQUEST)
79+
raise FailError(status_code=HTTPStatus.INTERNAL_SERVER_ERROR)
6280

6381
try:
6482
# Vuforia gives the same response for a body which is not UTF-8, such

tests/mock_vws/test_invalid_json.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,85 @@ def test_not_utf_8(endpoint: Endpoint) -> None:
141141
content = '{"name": "café"}'.encode(encoding="latin-1")
142142
_assert_body_rejected(endpoint=endpoint, content=content)
143143

144+
@staticmethod
145+
def test_empty_body(endpoint: Endpoint) -> None:
146+
"""Giving an empty body to an endpoint which takes JSON returns an
147+
error response.
148+
149+
Real Vuforia gives a server error for an empty body given to the
150+
target endpoints, but a bad request for one given to the reco counts
151+
report endpoint or to the VuMark instance generation endpoint.
152+
153+
Unlike the other malformed bodies which this class sends, an empty
154+
body is answered promptly by real Vuforia, so this test runs against
155+
it.
156+
"""
157+
takes_json_data = (
158+
endpoint.auth_header_content_type == "application/json"
159+
)
160+
if not takes_json_data:
161+
pytest.skip(reason="This endpoint does not take a JSON body.")
162+
163+
if endpoint.path_url.endswith("/instances"):
164+
expected_status_code = HTTPStatus.BAD_REQUEST
165+
expected_result_code = ResultCodes.BAD_REQUEST
166+
elif endpoint.path_url.endswith("/reports/recoCounts"):
167+
expected_status_code = HTTPStatus.BAD_REQUEST
168+
expected_result_code = ResultCodes.FAIL
169+
else:
170+
expected_status_code = HTTPStatus.INTERNAL_SERVER_ERROR
171+
expected_result_code = ResultCodes.FAIL
172+
173+
content = b""
174+
gmt = ZoneInfo(key="GMT")
175+
now = datetime.now(tz=gmt)
176+
time_to_freeze = now
177+
with freeze_time(time_to_freeze=time_to_freeze):
178+
date = rfc_1123_date()
179+
180+
authorization_string = authorization_header(
181+
access_key=endpoint.access_key,
182+
secret_key=endpoint.secret_key,
183+
method=endpoint.method,
184+
content=content,
185+
content_type=endpoint.auth_header_content_type,
186+
date=date,
187+
request_path=endpoint.path_url,
188+
)
189+
190+
new_headers = {
191+
**endpoint.headers,
192+
"Authorization": authorization_string,
193+
"Date": date,
194+
"Content-Length": str(object=len(content)),
195+
}
196+
197+
new_endpoint = Endpoint(
198+
base_url=endpoint.base_url,
199+
path_url=endpoint.path_url,
200+
method=endpoint.method,
201+
headers=new_headers,
202+
data=content,
203+
successful_headers_result_code=endpoint.successful_headers_result_code,
204+
successful_headers_status_code=endpoint.successful_headers_status_code,
205+
access_key=endpoint.access_key,
206+
secret_key=endpoint.secret_key,
207+
)
208+
209+
response = new_endpoint.send()
210+
211+
# A server error is the expected response for some endpoints, so
212+
# only treat one as transient where it is not expected.
213+
if expected_status_code != HTTPStatus.INTERNAL_SERVER_ERROR:
214+
handle_server_errors(response=response)
215+
216+
assert_valid_date_header(response=response)
217+
assert_vws_failure(
218+
response=response,
219+
status_code=expected_status_code,
220+
result_code=expected_result_code,
221+
)
222+
144223
@staticmethod
145224
def test_invalid_json_with_skewed_time(endpoint: Endpoint) -> None:
146225
"""Giving invalid JSON to endpoints returns error responses."""

0 commit comments

Comments
 (0)