From 36f31b203fa09a6e98ef5754c023bd754cdd9c1f Mon Sep 17 00:00:00 2001 From: JianYan <15005893916@163.com> Date: Thu, 10 Sep 2026 15:04:17 +0800 Subject: [PATCH] fix(api): check HTTP status before decoding POST responses --- src/browsergym/workarena/api/utils.py | 7 +-- tests/test_api_utils.py | 66 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 tests/test_api_utils.py diff --git a/src/browsergym/workarena/api/utils.py b/src/browsergym/workarena/api/utils.py index 47a6b2a..4c4f1e3 100644 --- a/src/browsergym/workarena/api/utils.py +++ b/src/browsergym/workarena/api/utils.py @@ -62,14 +62,15 @@ def table_api_call( params=params, json=json, ) + + # Check for HTTP success code before decoding the response body. + response.raise_for_status() + if method == "POST": sys_id = response.json()["result"]["sys_id"] data = {} params = {"sysparm_query": f"sys_id={sys_id}"} - # Check for HTTP success code (fail otherwise) - response.raise_for_status() - record_exists = False num_retries = 0 if method == "POST" or wait_for_record: diff --git a/tests/test_api_utils.py b/tests/test_api_utils.py new file mode 100644 index 0000000..b7d3e50 --- /dev/null +++ b/tests/test_api_utils.py @@ -0,0 +1,66 @@ +import json +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest +import requests + +from browsergym.workarena.api import utils + + +@pytest.fixture +def instance(): + return SimpleNamespace(snow_url="https://example.invalid", snow_credentials=("user", "pass")) + + +def make_response(status_code, content): + response = requests.Response() + response.status_code = status_code + response.url = "https://example.invalid/api/now/table/incident" + response._content = content + return response + + +@pytest.mark.parametrize("method", ["GET", "POST", "PUT", "DELETE"]) +@pytest.mark.parametrize( + "status_code, content", + [(401, b'{"error": {"message": "Unauthorized"}}'), (503, b"Unavailable")], +) +def test_table_api_raises_http_error_before_decoding( + monkeypatch, instance, method, status_code, content +): + response = make_response(status_code, content) + decode = Mock(wraps=response.json) + monkeypatch.setattr(response, "json", decode) + request = Mock(return_value=response) + sleep = Mock() + monkeypatch.setattr(utils.requests, "request", request) + monkeypatch.setattr(utils, "sleep", sleep) + + with pytest.raises(requests.HTTPError) as exc_info: + utils.table_api_call(instance, table="incident", method=method) + + assert exc_info.value.response is response + decode.assert_not_called() + sleep.assert_not_called() + assert request.call_count == 1 + + +def test_table_api_successful_post_still_waits_for_record(monkeypatch, instance): + created = {"result": {"sys_id": "record-123"}} + request = Mock( + side_effect=[ + make_response(201, json.dumps(created).encode()), + make_response(200, b'{"result": [{"sys_id": "record-123"}]}'), + ] + ) + monkeypatch.setattr(utils.requests, "request", request) + monkeypatch.setattr(utils, "sleep", Mock()) + + result = utils.table_api_call(instance, table="incident", method="POST") + + assert result == created + assert request.call_count == 2 + assert request.call_args_list[0].kwargs["method"] == "POST" + assert request.call_args_list[1].kwargs["method"] == "GET" + assert request.call_args_list[1].kwargs["params"] == {"sysparm_query": "sys_id=record-123"}