From 188ea63343275e877a7fbf0c86ef0b18bc28407d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sat, 24 Oct 2026 13:03:14 +0200 Subject: [PATCH] The HTTP API and CLI references state the write semantics: PATCH forms, PUT responses, upload limits and redirects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - documents: a PATCH is a single DELETE/INSERT … WHERE or DELETE WHERE, with GRAPH, WITH, USING and USING NAMED refused; PUT answers 204 on replace and 201 on create, with no ETag, while POST and PATCH answer with the document's new ETag; If-Match: * does not count as a precondition and an If-Match PUT to a missing document is refused 412; multipart bodies upload files and fall under the upload limit; the examples quote If-None-Match: * and If-Match - system endpoints: a settings PATCH takes the same forms and conditional-write terms as a document PATCH - configuration: MAX_CONTENT_LENGTH is in bytes and refuses with 413; MAX_UPLOAD_LENGTH caps multipart uploads and nginx follows it - CLI: redirects are not followed and count as failures, a 308 on a slash-less PUT writes nothing; add file appends by default and replaces with --replace - migrating, upload-file: If-Match: * is answered 428; the drop-zone refusal links the upload limit Co-Authored-By: Claude Fable 5.1 --- docs/reference/command-line-interface.ttl | 2 ++ docs/reference/configuration.ttl | 4 ++- docs/reference/http-api/documents.ttl | 27 +++++++++++++------- docs/reference/http-api/system-endpoints.ttl | 2 +- docs/reference/migrating-from-5x.ttl | 2 +- docs/user-guide/upload-file.ttl | 2 +- 6 files changed, 26 insertions(+), 13 deletions(-) diff --git a/docs/reference/command-line-interface.ttl b/docs/reference/command-line-interface.ttl index 24cf4db..a493a1a 100644 --- a/docs/reference/command-line-interface.ttl +++ b/docs/reference/command-line-interface.ttl @@ -143,6 +143,7 @@ ldh create container --parent "$LDH_BASE" --title "Categories" --slug categories
item=$(ldh create item --container "$LDH_BASE" --title "Example" --slug example)

ldh push writes many documents in one run and prints one line per written document URL or upload URI, in write order; progress and skipped entries go to standard error, and the first failed request stops the run.

Exit codes are 0 on success, 1 on an HTTP error status or runtime failure (the message goes to standard error; add --verbose for a stack trace), and 2 on a usage error.

+

Redirects are not followed and count as failures, with their Location on standard error: a PUT to a URI without a trailing slash is answered 308 Permanent Redirect and writes nothing. A write answered 303 See Other succeeds; ldh get --head reports any status, and ldh get --timegate takes its 302 Found.

Usage

@@ -398,6 +399,7 @@ ldh add file \\ "$doc"

add file prints the content-addressed URI of the upload ({base}uploads/{sha1}) rather than the document URI — the Data stage of the tutorial feeds exactly that URI into a CSV import.

+

It appends the file's description to the document (a multipart POST); with --replace it replaces the document's description with it instead (a multipart PUT).

See also the data import user guides.

Request bodies

ldh put and ldh post read RDF from an optional FILE argument, recognizing the syntax from its extension (ldh put "$LDH_BASE" root.ttl), or from standard input, where -t/--content-type is required since a stream carries no name. Either way relative URIs resolve against the target URI. ldh patch reads a SPARQL 1.1 update from standard input, validates it and sends it verbatim.

diff --git a/docs/reference/configuration.ttl b/docs/reference/configuration.ttl index 7b6b76d..ec03545 100644 --- a/docs/reference/configuration.ttl +++ b/docs/reference/configuration.ttl @@ -195,7 +195,9 @@ HOST=ec2-54-235-229-141.compute-1.amazonaws.com
Set to false if you are not using the self-signed server certificate (e.g. using a LetsEncrypt certificate instead). Not to be confused with the WebID client certificate. Defaults to true.
MAX_CONTENT_LENGTH
-
Maximum allowed request body size (nginx has a separate setting for this). Defaults to 2097152.
+
Maximum allowed request body size, in bytes; a larger body is refused with 413 Payload Too Large. Defaults to 2097152.
+
MAX_UPLOAD_LENGTH
+
Maximum size, in bytes, of a file upload (a multipart/form-data request body); a larger one is refused with 413 Payload Too Large. Defaults to MAX_CONTENT_LENGTH. nginx limits request bodies to this value when it is set, and to MAX_CONTENT_LENGTH otherwise.
MAX_QUERY_RESPONSE_LENGTH
The longest query result, in bytes, that the /sparql endpoint answers a query with; a longer one is refused with 502 Bad Gateway. Counted on the triplestore's response to the platform, not on what the caller receives. Defaults to 5242880 (5 MiB).
HTTP_MAX_THREADS
diff --git a/docs/reference/http-api/documents.ttl b/docs/reference/http-api/documents.ttl index 881fb75..ad60031 100644 --- a/docs/reference/http-api/documents.ttl +++ b/docs/reference/http-api/documents.ttl @@ -21,7 +21,8 @@

Methods

-

The API also supports the PATCH HTTP method which is optional in GSP. It accepts graph-scoped SPARQL updates that will modify the requested document. Only the INSERT/WHERE and DELETE WHERE forms are supported; GRAPH patterns are not allowed.

+

The API also supports the PATCH HTTP method which is optional in GSP. It accepts graph-scoped SPARQL updates that will modify the requested document. Only a single DELETE/INSERT … WHERE or DELETE WHERE operation is supported; GRAPH, WITH, USING and USING NAMED are not allowed (see graph-scoped updates).

+

A multipart/form-data body uploads files: a POST appends their descriptions to the document and a PUT replaces the document with them, as the same writes with an RDF body do. The upload limit applies to such a body instead of the request body limit (see configuration).

Trailing slashes in document URIs are enforced using 308 Permanent Redirect responses.

@@ -75,7 +76,7 @@ - + @@ -121,7 +122,7 @@ - + @@ -132,6 +133,7 @@
HTTP methods and the responses they return
PUT Upserts a document200 OK

201 Created

308 Permanent Redirect
204 No Content (replaced)

201 Created (created)

308 Permanent Redirect
400 Bad Request RDF syntax error
Modifies a document using SPARQL Update 204 No Content 422 Unprocessable EntitySPARQL update is not a single graph-scoped INSERT/DELETE operation, or removes an rdf:typeSPARQL update is not a single DELETE/INSERT … WHERE or DELETE WHERE operation, uses GRAPH, WITH, USING or USING NAMED, or removes an rdf:type
Constraint violation in the updated document

Any method may additionally answer 403 Forbidden — see Access control. Any method that writes may answer 428 Precondition Required when the request carries no precondition, or 412 Precondition Failed when the one it carries is out of date — see Conditional writes.

+

A POST or PATCH answers with the ETag of the document it leaves, so the next write can quote it. A PUT answers with no ETag or Last-Modified: the document it stores is not the payload — the server adds its metadata and skolemizes blank nodes — and RFC 9110 allows a PUT response a validator only for a representation saved unchanged. A client that writes again after a PUT reads the tag with HEAD.

Document metadata

@@ -145,6 +147,7 @@
PUT /regions/ HTTP/1.1
 Host: localhost:4443
 Content-Type: text/turtle
+If-None-Match: *
 
 @prefix dh:     <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
 @prefix dct:    <http://purl.org/dc/terms/> .
@@ -168,7 +171,8 @@ Content-Type: text/turtle
         

The HTTP request to produce a new item can be empty:

PUT /regions/1/ HTTP/1.1
 Host: localhost:4443
-Content-Type: text/turtle
+Content-Type: text/turtle +If-None-Match: *

It will create an item document with the following triples:

@prefix dh:     <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
 @prefix dct:    <http://purl.org/dc/terms/> .
@@ -185,10 +189,12 @@ Content-Type: text/turtle

Graph-scoped updates

-

A PATCH applies a SPARQL update scoped to the requested document's graph. The request body must contain a single update operation, in one of two forms: DELETE/INSERT with a WHERE clause (either template can be omitted, giving plain INSERT { } WHERE { } or DELETE { } WHERE { }), or the DELETE WHERE shorthand. Every other form is rejected with 422 Unprocessable Entity, as is a body with more than one operation. Appending a description to the Chai product:

+

A PATCH applies a SPARQL update scoped to the requested document's graph. The request body must contain a single update operation, in one of two forms: DELETE/INSERT with a WHERE clause (either template can be omitted, giving plain INSERT { } WHERE { } or DELETE { } WHERE { }), or the DELETE WHERE shorthand. Every other form is rejected with 422 Unprocessable Entity, as is a body with more than one operation. Like any write to an existing document, it quotes the document's entity tag (see Conditional writes). Appending a description to the Chai product:

PATCH /products/1/ HTTP/1.1
 Host: localhost:4443
+Accept: application/n-triples
 Content-Type: application/sparql-update
+If-Match: "1e635b4-ntriples"
 
 PREFIX schema: <https://schema.org/>
 
@@ -197,11 +203,13 @@ INSERT
     <https://localhost:4443/products/1/#this> schema:description "A delicate black tea blend" .
 }
 WHERE {}
-

The update executes against that one graph, so GRAPH patterns are not allowed — this request is rejected with +

The update executes against that one graph, so it cannot name another: GRAPH is not allowed in a template, in the WHERE pattern (including EXISTS and sub-queries) or in a DELETE WHERE, and neither are WITH, USING and USING NAMED. This request is rejected with 422 Unprocessable Entity:

PATCH /products/1/ HTTP/1.1
 Host: localhost:4443
+Accept: application/n-triples
 Content-Type: application/sparql-update
+If-Match: "1e635b4-ntriples"
 
 PREFIX schema: <https://schema.org/>
 
@@ -233,10 +241,10 @@ WHERE {}

A write to a document that already exists must say which state it was written against. Every write reads the document's graph, changes it in memory and writes the whole graph back, so two writers that name no precondition overwrite each other with nothing to show that anything was lost.

Such a write is answered 428 Precondition Required. To make it conditional, send either:

    -
  • If-Match with the document's current ETag, to modify what you read
  • +
  • If-Match with the document's current ETag, to modify what you read. If-Match: * asserts only that some state exists and does not count
  • If-None-Match: *, to create a document only if it does not exist yet
-

Creating a document needs no precondition — there is no prior state to have been written against — so a PUT to a URL that does not exist is accepted as it always was.

+

Creating a document needs no precondition — there is no prior state to have been written against — so a PUT to a URL that does not exist is accepted without one. A PUT that quotes an If-Match to a URL that does not exist is refused 412 Precondition Failed: the state it was written against is gone, and a document deleted in the meantime is not recreated.

If the document changed since you read it, the write is refused 412 Precondition Failed, and that response carries the document's current ETag so you can re-read, reapply your change and retry without an extra request. If-None-Match: * against a document that already exists is likewise refused 412, which is how a client can create-or-append in two steps.

@@ -255,7 +263,8 @@ Accept: application/n-triples Content-Type: application/sparql-update If-Match: "f731c06f5ac4fec6490dff1399bd5440" -HTTP/1.1 204 No Content +HTTP/1.1 204 No Content +ETag: "9b0e4d1c27a3f58e6c41d2a7b8f05e93"

HEAD is answered for any access mode the agent holds, not acl:Read alone, so an agent granted acl:Write or acl:Append can read the validator its writes have to quote without being able to read the document. GET still requires acl:Read.

The command line interface does this for you: every ldh command that writes reads the document's validator first and sends it.

diff --git a/docs/reference/http-api/system-endpoints.ttl b/docs/reference/http-api/system-endpoints.ttl index dbc6d7d..143e9c3 100644 --- a/docs/reference/http-api/system-endpoints.ttl +++ b/docs/reference/http-api/system-endpoints.ttl @@ -46,7 +46,7 @@ settings - The dataspace's own description in the dataspace configuration. PATCH with a SPARQL update edits it — this is how packages are installed and uninstalled. Editing is restricted to end-user dataspaces: a PATCH on an admin dataspace's settings answers 422 Unprocessable Entity + The dataspace's own description in the dataspace configuration. PATCH with a SPARQL update edits it — this is how packages are installed and uninstalled. The update takes the same forms as a document PATCH, and is held to the same conditional write terms: it quotes the settings' ETag in If-Match or is answered 428 Precondition Required, a stale tag is answered 412 Precondition Failed, and a successful update answers 204 No Content with the new ETag. Editing is restricted to end-user dataspaces: a PATCH on an admin dataspace's settings answers 422 Unprocessable Entity diff --git a/docs/reference/migrating-from-5x.ttl b/docs/reference/migrating-from-5x.ttl index c257e11..5613ad7 100644 --- a/docs/reference/migrating-from-5x.ttl +++ b/docs/reference/migrating-from-5x.ttl @@ -197,7 +197,7 @@

A write to a document that already exists must be conditional: If-Match with the document's current - ETag, or If-None-Match: * to create. An unconditional write is answered 428 Precondition Required.

+ ETag, or If-None-Match: * to create. An unconditional write, or one with If-Match: *, is answered 428 Precondition Required.

A client that wrote unconditionally now reads the tag first, as the ldh CLI does for you. An entity tag diff --git a/docs/user-guide/upload-file.ttl b/docs/user-guide/upload-file.ttl index 4b28d62..29b8961 100644 --- a/docs/user-guide/upload-file.ttl +++ b/docs/user-guide/upload-file.ttl @@ -33,7 +33,7 @@

  • Drop it. The document reloads in Read mode with the file among its resources; an image is shown inline
  • -

    A file larger than the instance's request limit is refused before it reaches the platform, and the page says so without navigating away.

    +

    A file larger than the instance's upload limit is refused before it reaches the platform, and the page says so without navigating away.

    Alternatively, upload through a form:

    1. Click the Create dropdown at the bottom of the page