From 7a02f611856902b3c8e73a173f8840c8e249a257 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Fri, 20 Dec 2024 19:27:21 -0500 Subject: [PATCH 1/2] fix PROV endpoints returning invalid double Content-Type headers --- CHANGES.rst | 4 +++- tests/functional/test_job_provenance.py | 11 +++++++++++ weaver/datatype.py | 2 +- weaver/wps_restapi/jobs/utils.py | 5 ++--- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 727bddfe7..2ad8e5cff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,7 +16,9 @@ Changes: Fixes: ------ -- No change. +- Fix ``PROV`` endpoints returning multiple ``Content-Type`` headers + (default ``text/html`` inserted by ``webob.response.Response`` class onto top of the explicit one specified) + leading to inconsistent responses parsing and rendering across clients. .. _changes_6.1.0: diff --git a/tests/functional/test_job_provenance.py b/tests/functional/test_job_provenance.py index eafb7d980..a3dc3c422 100644 --- a/tests/functional/test_job_provenance.py +++ b/tests/functional/test_job_provenance.py @@ -99,6 +99,7 @@ def test_job_prov_json(self, queries, headers): prov_url = f"{self.job_url}/prov" resp = self.app.get(prov_url, params=queries, headers=headers) assert resp.status_code == 200 + assert len(list(filter(lambda header: header[0] == "Content-Type", resp.headerlist))) == 1 assert resp.content_type == ContentType.APP_JSON prov = resp.json assert "prefix" in prov @@ -113,6 +114,7 @@ def test_job_prov_xml(self, queries, headers): prov_url = f"{self.job_url}/prov" resp = self.app.get(prov_url, params=queries, headers=headers) assert resp.status_code == 200 + assert len(list(filter(lambda header: header[0] == "Content-Type", resp.headerlist))) == 1 assert resp.content_type in ContentType.ANY_XML prov = resp.text assert " Tuple[Optional[str], Optional[AnyContentType]] """ diff --git a/weaver/wps_restapi/jobs/utils.py b/weaver/wps_restapi/jobs/utils.py index e63fac5bf..6cbf6fbea 100644 --- a/weaver/wps_restapi/jobs/utils.py +++ b/weaver/wps_restapi/jobs/utils.py @@ -1451,8 +1451,7 @@ def get_job_prov_response(request): prov_body["error"] = "No such run ID for specified job provenance." prov_body["value"] = {"run_id": str(request.matchdict["run_id"])} prov_body["status"] = prov_err.code - return prov_err(json=prov_body, headers={"Content-Type": ContentType.APP_JSON}) + return prov_err(json=prov_body, content_type=ContentType.APP_JSON) links = job.links(container=request, self_link="provenance") headers = [("Link", make_link_header(link)) for link in links] - headers.append(("Content-Type", prov_type)) - return HTTPOk(body=prov_data, headers=headers) + return HTTPOk(body=prov_data, headers=headers, content_type=prov_type, charset="utf-8") From ebebe7b2dad38cc1c4f260fa9a4febf0800456fd Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Fri, 20 Dec 2024 19:42:04 -0500 Subject: [PATCH 2/2] fix tests --- tests/functional/test_cli.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py index 316742523..74b2abde4 100644 --- a/tests/functional/test_cli.py +++ b/tests/functional/test_cli.py @@ -43,7 +43,7 @@ from weaver.config import WeaverConfiguration from weaver.datatype import DockerAuthentication, Service from weaver.execute import ExecuteReturnPreference -from weaver.formats import ContentType, OutputFormat, get_cwl_file_format, repr_json +from weaver.formats import ContentType, OutputFormat, clean_media_type_format, get_cwl_file_format, repr_json from weaver.notify import decrypt_email from weaver.processes.constants import CWL_REQUIREMENT_APP_DOCKER, ProcessSchema from weaver.processes.types import ProcessType @@ -2581,7 +2581,8 @@ def setUp(self): def test_prov(self): result = mocked_sub_requests(self.app, self.client.prov, self.job_url) assert result.success - assert result.headers["Content-Type"] == ContentType.APP_JSON + ctype = clean_media_type_format(result.headers["Content-Type"], strip_parameters=True) + assert ctype == ContentType.APP_JSON assert isinstance(result.body, dict), "body should be the PROV-JSON" assert "actedOnBehalfOf" in result.body assert "agent" in result.body @@ -2591,7 +2592,8 @@ def test_prov(self): def test_prov_yaml_by_output_format(self): result = mocked_sub_requests(self.app, self.client.prov, self.job_url, output_format=OutputFormat.YAML) assert result.success - assert result.headers["Content-Type"] == ContentType.APP_JSON, "original type should still be JSON (from API)" + ctype = clean_media_type_format(result.headers["Content-Type"], strip_parameters=True) + assert ctype == ContentType.APP_JSON, "original type should still be JSON (from API)" assert isinstance(result.body, dict), "response body should still be the original PROV-JSON" assert isinstance(result.text, str), "text property should be the PROV-JSON represented as YAML string" assert yaml.safe_load(result.text) == result.body, "PROV-JSON contents should be identical in YAML format" @@ -2603,7 +2605,8 @@ def test_prov_yaml_by_output_format(self): def test_prov_xml_by_prov_format(self): result = mocked_sub_requests(self.app, self.client.prov, self.job_url, prov_format=ProvenanceFormat.PROV_XML) assert result.success - assert result.headers["Content-Type"] == ContentType.APP_XML, "original type should still be XML (from API)" + ctype = clean_media_type_format(result.headers["Content-Type"], strip_parameters=True) + assert ctype == ContentType.APP_XML, "original type should still be XML (from API)" assert isinstance(result.body, str), "body should be the PROV-XML representation" assert "actedOnBehalfOf" in result.body assert "agent" in result.body @@ -2613,14 +2616,16 @@ def test_prov_xml_by_prov_format(self): def test_prov_info(self): result = mocked_sub_requests(self.app, self.client.prov, self.job_url, prov=ProvenancePathType.PROV_INFO) assert result.success - assert result.headers["Content-Type"] == ContentType.TEXT_PLAIN + ctype = clean_media_type_format(result.headers["Content-Type"], strip_parameters=True) + assert ctype == ContentType.TEXT_PLAIN assert "Research Object of CWL workflow run" in result.text assert self.job_id in result.text def test_prov_run(self): result = mocked_sub_requests(self.app, self.client.prov, self.job_url, prov=ProvenancePathType.PROV_RUN) assert result.success - assert result.headers["Content-Type"] == ContentType.TEXT_PLAIN + ctype = clean_media_type_format(result.headers["Content-Type"], strip_parameters=True) + assert ctype == ContentType.TEXT_PLAIN assert self.proc_id in result.text assert self.job_id in result.text assert "< wf:main/message" in result.text, ( @@ -2639,7 +2644,8 @@ def test_prov_run_with_id(self): prov_run_id=self.job_id, # redundant in this case, but test that parameter is parsed and resolves ) assert result.success - assert result.headers["Content-Type"] == ContentType.TEXT_PLAIN + ctype = clean_media_type_format(result.headers["Content-Type"], strip_parameters=True) + assert ctype == ContentType.TEXT_PLAIN assert self.proc_id in result.text assert self.job_id in result.text assert "< wf:main/message" in result.text, (