From 9c52fffd57a7069dad646d08040af64c3a5e394a Mon Sep 17 00:00:00 2001 From: sascha Date: Fri, 21 Nov 2025 14:57:53 +0100 Subject: [PATCH 1/5] correct Accept header for construct queries --- rdflib/plugins/stores/sparqlconnector.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/rdflib/plugins/stores/sparqlconnector.py b/rdflib/plugins/stores/sparqlconnector.py index 3dc2158f7..d259a0a4b 100644 --- a/rdflib/plugins/stores/sparqlconnector.py +++ b/rdflib/plugins/stores/sparqlconnector.py @@ -10,6 +10,7 @@ from urllib.request import Request, urlopen from rdflib.plugin import plugins +from rdflib.plugins.sparql import prepareQuery from rdflib.query import Result, ResultParser from rdflib.term import BNode from rdflib.util import FORMAT_MIMETYPE_MAP, RESPONSE_TABLE_FORMAT_MIMETYPE_MAP @@ -91,6 +92,11 @@ def query( params["default-graph-uri"] = default_graph headers = {"Accept": self.response_mime_types()} + + # change Accept header to an RDF mime type in case of a construct query + qtype = self.__get_query_type__(query) + if qtype in ("ConstructQuery", "DescribeQuery"): + headers.update({"Accept": self.response_mime_types_rdf()}) args = copy.deepcopy(self.kwargs) @@ -205,5 +211,23 @@ def response_mime_types(self) -> str: supported_formats.add(plugin.name) return ", ".join(supported_formats) + def response_mime_types_rdf(self) -> str: + """Construct a HTTP-Header Accept field to reflect the supported mime types for SPARQL construct/describe queries that return a graph. + + If the return_format parameter is set, the mime types are restricted to these accordingly. + """ + rdf_mimetype_map = [mime for mlist in FORMAT_MIMETYPE_MAP.values() for mime in mlist] + + # use the matched returnType if it matches one of the rdf mime types + if self.returnFormat in FORMAT_MIMETYPE_MAP: + return FORMAT_MIMETYPE_MAP[self.returnFormat][0] + else: + return ", ".join(rdf_mimetype_map) + + def __get_query_type__(self, query: str) -> str: + q = prepareQuery(query) + algebra = getattr(q, "algebra", None) + name = getattr(algebra, "name", None) # e.g. 'SelectQuery', 'ConstructQuery', 'DescribeQuery', 'AskQuery' + return name __all__ = ["SPARQLConnector", "SPARQLConnectorException"] From 08d245994e6b4d38b425746a973c605b97086556 Mon Sep 17 00:00:00 2001 From: sascha Date: Fri, 21 Nov 2025 14:58:36 +0100 Subject: [PATCH 2/5] register TurtleParser for legacy mime type 'application/x-turtle' --- rdflib/plugin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rdflib/plugin.py b/rdflib/plugin.py index f7b98c99f..f3c01f1dc 100644 --- a/rdflib/plugin.py +++ b/rdflib/plugin.py @@ -378,6 +378,12 @@ def plugins( "rdflib.plugins.parsers.notation3", "TurtleParser", ) +register( + "application/x-turtle", + Parser, + "rdflib.plugins.parsers.notation3", + "TurtleParser", +) register( "turtle", Parser, From e2d56fde64b74a00c087f04527424d565b41de22 Mon Sep 17 00:00:00 2001 From: sascha Date: Fri, 21 Nov 2025 16:01:04 +0100 Subject: [PATCH 3/5] catch exception when parsing query string for the query type --- rdflib/plugins/stores/sparqlconnector.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/rdflib/plugins/stores/sparqlconnector.py b/rdflib/plugins/stores/sparqlconnector.py index d259a0a4b..92ae0f098 100644 --- a/rdflib/plugins/stores/sparqlconnector.py +++ b/rdflib/plugins/stores/sparqlconnector.py @@ -224,10 +224,13 @@ def response_mime_types_rdf(self) -> str: else: return ", ".join(rdf_mimetype_map) - def __get_query_type__(self, query: str) -> str: - q = prepareQuery(query) - algebra = getattr(q, "algebra", None) - name = getattr(algebra, "name", None) # e.g. 'SelectQuery', 'ConstructQuery', 'DescribeQuery', 'AskQuery' - return name + def __get_query_type__(self, query: str) -> str | None: + try: + q = prepareQuery(query) + algebra = getattr(q, "algebra", None) + name = getattr(algebra, "name", None) + return name # e.g. 'SelectQuery', 'ConstructQuery', 'DescribeQuery', 'AskQuery' + except Exception: + log.debug(f"cannot parse query: {query}") __all__ = ["SPARQLConnector", "SPARQLConnectorException"] From 3134f4d5120e9feb13e3d32a4ef21758bca34f0b Mon Sep 17 00:00:00 2001 From: sascha Date: Thu, 27 Nov 2025 17:00:53 +0100 Subject: [PATCH 4/5] test case for sparql construct query accept header --- .../test_store_sparqlstore_query.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/test_store/test_store_sparqlstore_query.py b/test/test_store/test_store_sparqlstore_query.py index d17823087..5a2647ad9 100644 --- a/test/test_store/test_store_sparqlstore_query.py +++ b/test/test_store/test_store_sparqlstore_query.py @@ -127,3 +127,50 @@ def test_query_construct_format( logging.debug("request = %s", request) logging.debug("request.headers = %s", request.headers.as_string()) assert request.path_query["query"][0] == query + +def test_query_construct_accept_header( + function_httpmock: ServedBaseHTTPServerMock +) -> None: + """ + Test that no SPARQL result media types are used for construct queries + """ + graph = Graph( + "SPARQLStore", + identifier="http://example.com", + bind_namespaces="none", + ) + url = f"{function_httpmock.url}/query" + graph.open(url) + + function_httpmock.responses[MethodName.GET].extend( + [ + MockHTTPResponse( + 200, + "OK", + b"<> a <#test> .", + {"Content-Type": ["text/turtle"]}, + ) + ] * 2 # two identical responses + ) + + # case 1: construct query + + query_construct = "CONSTRUCT WHERE { ?s ?p ?o }" + graph.query(query_construct) + + request_construct = function_httpmock.requests[MethodName.GET].pop() + accept_header_construct = request_construct.headers.get('Accept').lower() + # 'Accept' header must not include types for XML or JSON sparql results + assert "application/sparql-results" not in accept_header_construct + # 'Accept' header should be at least the default RDF/XML + assert "application/rdf+xml" in accept_header_construct + + # case 2: select query + + query_select = "SELECT * WHERE { ?s ?p ?o }" + graph.query(query_select) + + request_select = function_httpmock.requests[MethodName.GET].pop() + accept_header_select = request_select.headers.get('Accept').lower() + # 'Accept' header should include types for XML or JSON sparql results + assert "application/sparql-results" in accept_header_select \ No newline at end of file From 4ee9f783aa4904c01df9dcb4fa6e3954e13f9ac7 Mon Sep 17 00:00:00 2001 From: sascha Date: Mon, 1 Dec 2025 11:39:39 +0100 Subject: [PATCH 5/5] fix mypy errors and formatting --- rdflib/plugins/stores/sparqlconnector.py | 12 ++++++++---- test/test_store/test_store_sparqlstore_query.py | 12 +++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/rdflib/plugins/stores/sparqlconnector.py b/rdflib/plugins/stores/sparqlconnector.py index 92ae0f098..569e6e7d7 100644 --- a/rdflib/plugins/stores/sparqlconnector.py +++ b/rdflib/plugins/stores/sparqlconnector.py @@ -92,7 +92,7 @@ def query( params["default-graph-uri"] = default_graph headers = {"Accept": self.response_mime_types()} - + # change Accept header to an RDF mime type in case of a construct query qtype = self.__get_query_type__(query) if qtype in ("ConstructQuery", "DescribeQuery"): @@ -216,7 +216,9 @@ def response_mime_types_rdf(self) -> str: If the return_format parameter is set, the mime types are restricted to these accordingly. """ - rdf_mimetype_map = [mime for mlist in FORMAT_MIMETYPE_MAP.values() for mime in mlist] + rdf_mimetype_map = [ + mime for mlist in FORMAT_MIMETYPE_MAP.values() for mime in mlist + ] # use the matched returnType if it matches one of the rdf mime types if self.returnFormat in FORMAT_MIMETYPE_MAP: @@ -228,9 +230,11 @@ def __get_query_type__(self, query: str) -> str | None: try: q = prepareQuery(query) algebra = getattr(q, "algebra", None) - name = getattr(algebra, "name", None) - return name # e.g. 'SelectQuery', 'ConstructQuery', 'DescribeQuery', 'AskQuery' + name = getattr(algebra, "name", None) + return name # e.g. 'SelectQuery', 'ConstructQuery', 'DescribeQuery', 'AskQuery' except Exception: log.debug(f"cannot parse query: {query}") + return None + __all__ = ["SPARQLConnector", "SPARQLConnectorException"] diff --git a/test/test_store/test_store_sparqlstore_query.py b/test/test_store/test_store_sparqlstore_query.py index 5a2647ad9..1f16831df 100644 --- a/test/test_store/test_store_sparqlstore_query.py +++ b/test/test_store/test_store_sparqlstore_query.py @@ -128,8 +128,9 @@ def test_query_construct_format( logging.debug("request.headers = %s", request.headers.as_string()) assert request.path_query["query"][0] == query + def test_query_construct_accept_header( - function_httpmock: ServedBaseHTTPServerMock + function_httpmock: ServedBaseHTTPServerMock, ) -> None: """ Test that no SPARQL result media types are used for construct queries @@ -150,7 +151,8 @@ def test_query_construct_accept_header( b"<> a <#test> .", {"Content-Type": ["text/turtle"]}, ) - ] * 2 # two identical responses + ] + * 2 # two identical responses ) # case 1: construct query @@ -159,7 +161,7 @@ def test_query_construct_accept_header( graph.query(query_construct) request_construct = function_httpmock.requests[MethodName.GET].pop() - accept_header_construct = request_construct.headers.get('Accept').lower() + accept_header_construct = request_construct.headers.get("Accept", "").lower() # 'Accept' header must not include types for XML or JSON sparql results assert "application/sparql-results" not in accept_header_construct # 'Accept' header should be at least the default RDF/XML @@ -171,6 +173,6 @@ def test_query_construct_accept_header( graph.query(query_select) request_select = function_httpmock.requests[MethodName.GET].pop() - accept_header_select = request_select.headers.get('Accept').lower() + accept_header_select = request_select.headers.get("Accept", "").lower() # 'Accept' header should include types for XML or JSON sparql results - assert "application/sparql-results" in accept_header_select \ No newline at end of file + assert "application/sparql-results" in accept_header_select