HTTP API
- Methods
- Response headers & conditional requests
- Content negotiation
- Errors
- Discovery — the storage description
- Integrity — RFC 9530 digest fields
- Examples
Methods
| Method | Target | Behaviour |
|---|---|---|
GET / HEAD |
data resource | Content-negotiated representation (Turtle, JSON-LD, N-Triples, RDF/XML); binary streamed as-is, with byte-range support (206/416, Accept-Ranges: bytes). |
GET / HEAD |
/ (the storage URI) |
The storage description (application/lws+cid), unless the client asks for a container representation — application/lws+json, application/ld+json, application/json or an RDF type — in which case the root container listing below. |
GET / HEAD |
container | application/lws+json listing (id/type/totalItems/items[], each with id/type/format/size/modified, type naming any declared types after DataResource/Container); content-negotiable as application/ld+json / application/json (the requested Content-Type is echoed) or RDF; paginated (?page=N, Link rel first/prev/next/last) above lws.container.page-size. |
POST |
a container | Create a contained resource; Slug names it, Link: rel="type" picks container/RDF/non-RDF; 201 + Location. A reserved name is refused with 409. |
PUT |
any IRI | Create (new) or replace (existing) at that exact IRI; replacing MUST be conditional. The parent container must already exist (404 otherwise; 409 if the parent is not a container). A reserved path is refused with 409; an existing container with 409 (its representation is its membership); a Link: rel="type" that contradicts the IRI’s own shape with 400. |
PATCH |
RDF or JSON resource | RDF: application/sparql-update only (JSON Merge Patch is not defined over RDF and is not accepted there); JSON & linkset: application/merge-patch+json (RFC 7386) or application/json-patch+json (RFC 6902). See Metadata & Linksets. |
DELETE |
any resource | Delete (non-empty container → 409, or recursive with Depth: infinity); removes the resource’s metadata too. |
GET/HEAD/PATCH/PUT/OPTIONS |
<resource>.meta |
The resource’s linkset (metadata) resource — see Metadata & Linksets. |
OPTIONS |
any | Allow, Accept-Post, Accept-Patch, Want-Content-Digest. |
POST |
<system-prefix>/token |
OAuth 2.0 Token Exchange (RFC 8693) at the embedded authorization server — see Authentication. |
GET |
/.well-known/lws-configuration |
The embedded authorization server’s metadata (RFC 8414). |
QUERY |
<system-prefix>/type-search |
Type Search — see Search & Type Index. |
The interaction model in Link: rel="type" may be given as https://www.w3.org/ns/lws#Container
or the LDP terms (ldp:Container/ldp:BasicContainer, ldp:RDFSource, ldp:NonRDFSource); a
trailing slash on the IRI or Slug also makes a container.
Reserved names
Several namespaces belong to the server, and no resource may be created or replaced in them —
POST with such a Slug, and PUT at such a path, are answered 409 Conflict:
| Reserved | Owned by |
|---|---|
any name ending .acl |
the access-control resource of its target |
any name ending .meta |
the linkset (metadata) resource of its target |
lws.system-prefix (default /.lws) and below |
the storage description, JWKS, token endpoint, subscriptions, type index/search, access requests/grants |
/app and /callback and below |
the management console and the OIDC login callback |
/.well-known/lws-configuration and /.well-known/acme-challenge and below |
the authorization server metadata and the ACME HTTP-01 responder |
The match ignores case and covers the container form (.acl/); the name is tested exactly as sent,
with no percent-decoding, so x%2Eacl is a distinct and legal name.
Response headers & conditional requests
Responses carry ETag, Last-Modified, and Link relations: rel="type" — the interaction
models, https://www.w3.org/ns/lws#Container/#DataResource, and any type the resource’s metadata
declares —, rel="https://www.w3.org/ns/lws#storage" to the storage URI (on every GET and HEAD
of a storage resource, linksets, ACLs and the server’s own containers included), rel="up" (parent
container, non-root), and rel="linkset" (the metadata resource); WAC adds rel="acl".
If-None-Match/If-Modified-Since→304 Not Modified.If-Match(stale) →412 Precondition Failed. A precondition against a resource that does not exist is never satisfied — includingIf-Match: *— so such aPUTgets412rather than creating the resource; an unconditionalPUTstill creates.- An unconditional PUT replacing an existing resource is refused with
428 Precondition Required(you must sendIf-Match). - A write exceeding
lws.quota.max-bytes→507 Insufficient Storage.
If-Match is a compare-and-swap: the tag is compared inside the transaction that makes the change,
so of two clients that both read "v1" and both PUT with If-Match: "v1", exactly one gets 204
and the other 412. The same holds for PATCH, DELETE and linkset writes.
A container’s ETag changes whenever its membership does, so GET a container and If-Match that
tag on a write to it. A numbered page (?page=N) carries its own tag, which deliberately does not
satisfy an If-Match on the container; for a paginated container take the tag from the RDF rendering
(Accept: text/turtle), which is the full listing and never paginated. Container listings are
filtered per client and sent Cache-Control: private, no-store.
A write that cannot be carried out is refused rather than partly performed: a PUT/POST creating
or replacing a container with a body is 409; a SPARQL Update that names a graph (GRAPH,
WITH, USING, graph management) is 400; a request body carrying RDF outside the default graph
(TriG GRAPH blocks, JSON-LD named graphs) is 400. Deleting a resource removes its content, ACL and
linkset in one transaction.
Non-RDF resources keep their stored media type but are served defensively: every one carries
X-Content-Type-Options: nosniff and Content-Security-Policy: sandbox, and anything a browser would
execute in this origin (text/html, image/svg+xml, application/xhtml+xml, XML) also gets
Content-Disposition: attachment.
Content negotiation
RDF resources are served in Turtle, JSON-LD, N-Triples, RDF/XML or TriG by Accept; the default is
Turtle. Container listings default to application/lws+json and also satisfy application/ld+json
and application/json (the requested type is echoed), or any RDF serialisation.
Errors
Errors are returned as application/problem+json (RFC 9457).
A 401 carries the lws10-core challenge — WWW-Authenticate: Bearer as_uri="<authorization
server>", realm="<storage URI>" (and the same for DPoP, with its algs) — and the
rel="https://www.w3.org/ns/lws#storage" Link, so a client learns where to get an access token and
what to ask for without a hardcoded URI:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer as_uri="https://storage.example", realm="https://storage.example/"
WWW-Authenticate: DPoP as_uri="https://storage.example", realm="https://storage.example/", algs="ES256 ES384 ES512 EdDSA RS256 RS384 RS512 PS256 PS384 PS512"
Link: <https://storage.example/>; rel="https://www.w3.org/ns/lws#storage"
Discovery — the storage description
The storage description is a W3C controlled identifier (CID) document (lws10-core, Storage
Description Resource), served as application/lws+cid at the storage URI — the server’s root,
/ — and at <system-prefix>/storage-description (default /.lws/storage-description). At /,
Accept decides: application/lws+cid (or no preference) gets the description, a container type
(application/lws+json, application/ld+json, application/json, RDF) gets the root container.
application/ld+json, application/json and RDF renderings of the description itself are available
by content negotiation at /.lws/storage-description.
Its @context is ["https://www.w3.org/ns/cid/v1", "https://www.w3.org/ns/lws/v1"] and its id is
the storage URI.
- The
servicearray advertises the endpoints on the storage:StorageRoot(the root container), theNotificationService, theTypeIndexService/TypeSearchService, theAccessRequestService/AccessGrantService, and — when enabled — the embedded SPARQL endpoint. Each carries atypeand aserviceEndpoint. verificationMethodpublishes the webhook signing key as aJsonWebKey, referenced fromauthentication— the key a webhook receiver checks notification signatures against.- The
capabilityarray uses structured objects ({ type, … }): the implemented specifications (type only; which authentication suites appear depends on configuration); aPatchSupportentry mapping each targetformatto its accepted PATCH formats; oneContentNegotiationentry per RDF serialisation (source→targetlist); and an RFC 9530 digest entry listing the supported algorithms.
{
"@context": ["https://www.w3.org/ns/cid/v1", "https://www.w3.org/ns/lws/v1"],
"id": "https://storage.example/",
"type": "Storage",
"verificationMethod": [
{ "id": "https://storage.example/#iMd12Qh…", "type": "JsonWebKey",
"controller": "https://storage.example/",
"publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "alg": "EdDSA", "kid": "iMd12Qh…", "x": "…" } }
],
"authentication": ["https://storage.example/#iMd12Qh…"],
"capability": [
{ "type": "https://w3c.github.io/lws-protocol/lws10-core/" },
{ "type": "https://w3c.github.io/lws-protocol/lws10-index/" },
{ "type": "https://www.w3.org/ns/lws#PatchSupport",
"format": { "text/turtle": ["application/sparql-update"],
"application/json": ["application/merge-patch+json", "application/json-patch+json"] } },
{ "type": "https://www.w3.org/ns/lws#ContentNegotiation",
"source": "text/turtle", "target": ["application/ld+json", "application/n-triples", "application/rdf+xml", "application/trig"] },
{ "type": "https://www.rfc-editor.org/info/rfc9530", "algorithm": ["sha-256", "sha-512"] }
],
"service": [
{ "type": "StorageRoot", "serviceEndpoint": "https://storage.example/" },
{ "type": "NotificationService", "serviceEndpoint": "https://storage.example/.lws/subscriptions",
"subscriptionType": ["WebhookSubscription"] },
{ "type": "TypeIndexService", "serviceEndpoint": "https://storage.example/.lws/type-index" },
{ "type": "TypeSearchService", "serviceEndpoint": "https://storage.example/.lws/type-search" },
{ "type": "AccessRequestService", "serviceEndpoint": "https://storage.example/.lws/access-requests",
"conformsTo": ["https://www.w3.org/ns/lws#AccessProfile"] },
{ "type": "AccessGrantService", "serviceEndpoint": "https://storage.example/.lws/access-grants",
"conformsTo": ["https://www.w3.org/ns/lws#AccessProfile"] }
]
}
Integrity — RFC 9530 digest fields
The server supports RFC 9530 Content-Digest /
Repr-Digest for sha-256 and sha-512:
- A request that carries a
Content-Digesthas its body verified before any write — a mismatch or a malformed field is rejected with400; digest algorithms the server does not support are ignored. - Reads honour
Want-Repr-Digest/Want-Content-Digestby emitting the correspondingRepr-Digest/Content-Digest(highest-weight preference; ties favour the stronger algorithm). - Non-RDF resources serve a
Repr-Digestcomputed from a SHA-256 persisted at write time, so the blob is never re-read;Content-Digestis omitted on partial206range responses (it would not describe the bytes actually sent). - The server advertises that it accepts integrity-protected writes by emitting
Want-Content-DigestonOPTIONSand on write responses, and lists digest support as a capability above.
Examples
# create an RDF resource in the root container
curl -i -X POST -H 'Content-Type: text/turtle' -H 'Slug: greeting' \
--data '<#it> <http://schema.org/name> "Hello LWS" .' http://localhost:8080/
# read it back as JSON-LD
curl -H 'Accept: application/ld+json' http://localhost:8080/greeting
# create a container, then a resource in it with PUT, then patch it (SPARQL Update)
curl -X PUT -H 'Link: <https://www.w3.org/ns/lws#Container>; rel="type"' http://localhost:8080/box/
curl -X PUT -H 'Content-Type: text/turtle' --data '<#x> <http://schema.org/n> 1 .' http://localhost:8080/box/x
curl -X PATCH -H 'Content-Type: application/sparql-update' \
--data 'INSERT DATA { <http://localhost:8080/box/x#x> <http://schema.org/age> 42 }' http://localhost:8080/box/x
# create a JSON resource, then partially update it with JSON Merge Patch (RFC 7386)
curl -X POST -H 'Content-Type: application/json' -H 'Slug: doc.json' \
--data '{"a":1,"b":2}' http://localhost:8080/
curl -X PATCH -H 'Content-Type: application/merge-patch+json' \
--data '{"b":null,"c":3}' http://localhost:8080/doc.json # => {"a":1,"c":3}
# replacing an existing resource needs If-Match (428 without it)
ETAG=$(curl -s -o /dev/null -w '%header{etag}' http://localhost:8080/doc.json)
curl -X PUT -H "If-Match: $ETAG" -H 'Content-Type: application/json' --data '{"a":2}' http://localhost:8080/doc.json
# ask for a representation digest on read
curl -i -H 'Want-Repr-Digest: sha-256=1' http://localhost:8080/greeting