Repository: RDFLib/rdflib

Issue:
Missing Content-Type header for SPARQLConnector update method (?)
In rdflib 4.2.2 the SPARQLWrapper added an applicable content-type header when sending queries and updates to a SPARQL endpoint.
The 5.0.0 the SPARQLConnector does not add a content-type and, since the GraphDB we use as SPARQL endpoint rejects update requests when Content-Type: application/sparql-update is not added to the header, I had to monkey patch the update method to forcebily add the content type to the header.

I also tried to add a headers parameter with the content-type to the SPARQLUpdateStore parameter when constructing a SPARQLUpdateStore instance, but this then the content type is also used for query request, which results in rejected query requests in case the store instance is used for querying.

Should the the content type be added by the SPARQLConnector like the SPARQLWrapper did or am i missing an other intended solution?

Best regards,
Arnoud


Discussion:

Patch:
diff --git a/rdflib/plugins/stores/sparqlconnector.py b/rdflib/plugins/stores/sparqlconnector.py
index ee981419..abb69a55 100644
--- a/rdflib/plugins/stores/sparqlconnector.py
+++ b/rdflib/plugins/stores/sparqlconnector.py
@@ -87,6 +87,7 @@ class SPARQLConnector(object):
         if self.method == 'GET':
             args['params'].update(params)
         elif self.method == 'POST':
+            args['headers'].update({'Content-Type': 'application/sparql-query'})
             args['data'] = params
         else:
             raise SPARQLConnectorException("Unknown method %s" % self.method)
@@ -106,7 +107,10 @@ class SPARQLConnector(object):
         if default_graph:
             params["using-graph-uri"] = default_graph
 
-        headers = {'Accept': _response_mime_types[self.returnFormat]}
+        headers = {
+            'Accept': _response_mime_types[self.returnFormat],
+            'Content-Type': 'application/sparql-update',
+        }
 
         args = dict(self.kwargs)
 


Test patch:
diff --git a/test/test_sparqlstore.py b/test/test_sparqlstore.py
index 26a69460..a0a93b57 100644
--- a/test/test_sparqlstore.py
+++ b/test/test_sparqlstore.py
@@ -4,7 +4,10 @@ import os
 import unittest
 from nose import SkipTest
 from requests import HTTPError
-
+from http.server import BaseHTTPRequestHandler, HTTPServer
+import socket
+from threading import Thread
+import requests
 
 try:
     assert len(urlopen("http://dbpedia.org/sparql").read()) > 0
@@ -67,5 +70,78 @@ class SPARQLStoreDBPediaTestCase(unittest.TestCase):
             assert type(i[0]) == Literal, i[0].n3()
 
 
+class SPARQLStoreUpdateTestCase(unittest.TestCase):
+    def setUp(self):
+        port = self.setup_mocked_endpoint()
+        self.graph = Graph(store="SPARQLUpdateStore", identifier=URIRef("urn:ex"))
+        self.graph.open(("http://localhost:{port}/query".format(port=port),
+            "http://localhost:{port}/update".format(port=port)), create=False)
+        ns = list(self.graph.namespaces())
+        assert len(ns) > 0, ns
+
+    def setup_mocked_endpoint(self):
+        # Configure mock server.
+        s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM)
+        s.bind(('localhost', 0))
+        address, port = s.getsockname()
+        s.close()
+        mock_server = HTTPServer(('localhost', port), SPARQL11ProtocolStoreMock)
+
+        # Start running mock server in a separate thread.
+        # Daemon threads automatically shut down when the main process exits.
+        mock_server_thread = Thread(target=mock_server.serve_forever)
+        mock_server_thread.setDaemon(True)
+        mock_server_thread.start()
+        print("Started mocked sparql endpoint on http://localhost:{port}/".format(port=port))
+        return port
+
+    def tearDown(self):
+        self.graph.close()
+
+    def test_Query(self):
+        query = "insert data {<urn:s> <urn:p> <urn:o>}"
+        res = self.graph.update(query)
+        print(res)
+
+
+class SPARQL11ProtocolStoreMock(BaseHTTPRequestHandler):
+    def do_POST(self):
+        """
+        If the body should be analysed as well, just use:
+        ```
+        body = self.rfile.read(int(self.headers['Content-Length'])).decode()
+        print(body)
+        ```
+        """
+        contenttype = self.headers.get("Content-Type")
+        if self.path == "/query":
+            if self.headers.get("Content-Type") == "application/sparql-query":
+                pass
+            elif self.headers.get("Content-Type") == "application/x-www-form-urlencoded":
+                pass
+            else:
+                self.send_response(requests.codes.not_acceptable)
+                self.end_headers()
+        elif self.path == "/update":
+            if self.headers.get("Content-Type") == "application/sparql-update":
+                pass
+            elif self.headers.get("Content-Type") == "application/x-www-form-urlencoded":
+                pass
+            else:
+                self.send_response(requests.codes.not_acceptable)
+                self.end_headers()
+        else:
+            self.send_response(requests.codes.not_found)
+            self.end_headers()
+        self.send_response(requests.codes.ok)
+        self.end_headers()
+        return
+
+    def do_GET(self):
+        # Process an HTTP GET request and return a response with an HTTP 200 status.
+        self.send_response(requests.codes.ok)
+        self.end_headers()
+        return
+
 if __name__ == '__main__':
     unittest.main()