The inconsistency
JSS is "JSON-LD native" by design — `--conneg` is off by default (`src/config.js:29`: `conneg: false`). The stance is: if you want to read/write Turtle, opt in explicitly.
But the PUT handler today doesn't enforce that stance. Looking at `src/handlers/resource.js:638-647`:
```js
const inputType = contentType.split(';')[0].trim().toLowerCase();
if (connegEnabled && (inputType === RDF_TYPES.TURTLE || inputType === RDF_TYPES.N3)) {
try {
const jsonLd = await toJsonLd(content, contentType, resourceUrl, connegEnabled);
content = Buffer.from(JSON.stringify(jsonLd, null, 2));
} catch (e) { /* 400 */ }
}
// … goes on to storage.write(storagePath, content) unconditionally
```
So with `--conneg` off:
- Client PUTs `Content-Type: text/turtle` body.
- The if-block is skipped (conneg disabled).
- `content` is still the raw Turtle bytes.
- JSS writes those bytes to disk at, say, `foo.jsonld` → now the file on disk is Turtle content with a `.jsonld` URL.
- Subsequent GET: JSS reads the file, `getContentType()` sees `.jsonld` → says `application/ld+json`. Serves Turtle bytes with a JSON-LD content-type header. Parser on the client errors.
So `--conneg=false` + Turtle PUT = silent corruption.
Proposal
When `--conneg` is off, reject non-JSON-LD PUT bodies on RDF routes with `415 Unsupported Media Type` and an `Accept-Post` / `Accept-Patch` header listing `application/ld+json`. Makes the server's contract explicit:
"I am JSON-LD native. Start me with `--conneg` if you want Turtle / N3 translation."
Rough shape:
```js
const rdfInputTypes = [RDF_TYPES.JSON_LD, 'application/json'];
if (!connegEnabled && contentType && !rdfInputTypes.includes(inputType) && looksLikeRdfResource(resourceUrl)) {
return reply
.code(415)
.header('Accept', 'application/ld+json')
.send({ error: 'Unsupported Media Type',
message: 'This server is JSON-LD native. Start with --conneg to accept text/turtle or text/n3.' });
}
```
`looksLikeRdfResource()` is the small new helper — e.g. extension in `{.jsonld, .json, .ttl, .n3, .acl, .meta}`, or no extension and path looks like a Solid resource (profile/card, …). Out of scope to get exhaustive; start with the common set.
Non-goals
- Not changing default mode. `conneg: false` stays the default.
- Not changing behaviour when `--conneg` is on — that path is fine (translation happens).
- Not touching binary / non-RDF uploads (images, text, etc.). The guard is specifically for RDF-shaped resources.
Why not fold this into #294
Scope
~20 lines in `handlePut` + a helper. Plus one test per rejected case. Small, but touches the PUT path, so the full suite is worth running.
Happy to PR after #294.
The inconsistency
JSS is "JSON-LD native" by design — `--conneg` is off by default (`src/config.js:29`: `conneg: false`). The stance is: if you want to read/write Turtle, opt in explicitly.
But the PUT handler today doesn't enforce that stance. Looking at `src/handlers/resource.js:638-647`:
```js
const inputType = contentType.split(';')[0].trim().toLowerCase();
if (connegEnabled && (inputType === RDF_TYPES.TURTLE || inputType === RDF_TYPES.N3)) {
try {
const jsonLd = await toJsonLd(content, contentType, resourceUrl, connegEnabled);
content = Buffer.from(JSON.stringify(jsonLd, null, 2));
} catch (e) { /* 400 */ }
}
// … goes on to storage.write(storagePath, content) unconditionally
```
So with `--conneg` off:
So `--conneg=false` + Turtle PUT = silent corruption.
Proposal
When `--conneg` is off, reject non-JSON-LD PUT bodies on RDF routes with `415 Unsupported Media Type` and an `Accept-Post` / `Accept-Patch` header listing `application/ld+json`. Makes the server's contract explicit:
Rough shape:
```js
const rdfInputTypes = [RDF_TYPES.JSON_LD, 'application/json'];
if (!connegEnabled && contentType && !rdfInputTypes.includes(inputType) && looksLikeRdfResource(resourceUrl)) {
return reply
.code(415)
.header('Accept', 'application/ld+json')
.send({ error: 'Unsupported Media Type',
message: 'This server is JSON-LD native. Start with --conneg to accept text/turtle or text/n3.' });
}
```
`looksLikeRdfResource()` is the small new helper — e.g. extension in `{.jsonld, .json, .ttl, .n3, .acl, .meta}`, or no extension and path looks like a Solid resource (profile/card, …). Out of scope to get exhaustive; start with the common set.
Non-goals
Why not fold this into #294
Scope
~20 lines in `handlePut` + a helper. Plus one test per rejected case. Small, but touches the PUT path, so the full suite is worth running.
Happy to PR after #294.