javascript-solid-server 0.0.155 → 0.0.156
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/handlers/resource.js +43 -31
- package/test/conneg.test.js +92 -0
package/package.json
CHANGED
package/src/handlers/resource.js
CHANGED
|
@@ -149,17 +149,18 @@ export async function handleGet(request, reply) {
|
|
|
149
149
|
const content = await storage.read(indexPath);
|
|
150
150
|
const indexStats = await storage.stat(indexPath);
|
|
151
151
|
|
|
152
|
-
//
|
|
152
|
+
// Pick the negotiated RDF type using q-aware Accept parsing. The
|
|
153
|
+
// naive `acceptHeader.includes('text/turtle')` we used to do here
|
|
154
|
+
// ignored q-weights — `Accept: application/ld+json, text/turtle;q=0.1`
|
|
155
|
+
// would still pick Turtle even though JSON-LD was preferred (#325).
|
|
153
156
|
const acceptHeader = request.headers.accept || '';
|
|
154
|
-
const
|
|
155
|
-
acceptHeader
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
acceptHeader.includes('application/json')
|
|
162
|
-
);
|
|
157
|
+
const negotiated = connegEnabled
|
|
158
|
+
? selectContentType(acceptHeader, true)
|
|
159
|
+
: null;
|
|
160
|
+
const wantsTurtle = negotiated === RDF_TYPES.TURTLE
|
|
161
|
+
|| negotiated === RDF_TYPES.N3
|
|
162
|
+
|| negotiated === 'application/n-triples';
|
|
163
|
+
const wantsJsonLd = negotiated === RDF_TYPES.JSON_LD;
|
|
163
164
|
|
|
164
165
|
if (wantsTurtle || wantsJsonLd) {
|
|
165
166
|
// Extract JSON-LD from HTML data island
|
|
@@ -257,13 +258,14 @@ export async function handleGet(request, reply) {
|
|
|
257
258
|
return reply.type('text/html').send(html);
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
//
|
|
261
|
+
// Pick the negotiated RDF type using q-aware Accept parsing (#325).
|
|
261
262
|
const acceptHeader = request.headers.accept || '';
|
|
262
|
-
const
|
|
263
|
-
acceptHeader
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
const negotiated = connegEnabled
|
|
264
|
+
? selectContentType(acceptHeader, true)
|
|
265
|
+
: null;
|
|
266
|
+
const wantsTurtle = negotiated === RDF_TYPES.TURTLE
|
|
267
|
+
|| negotiated === RDF_TYPES.N3
|
|
268
|
+
|| negotiated === 'application/n-triples';
|
|
267
269
|
|
|
268
270
|
if (wantsTurtle) {
|
|
269
271
|
// Convert container JSON-LD to Turtle
|
|
@@ -383,11 +385,14 @@ export async function handleGet(request, reply) {
|
|
|
383
385
|
if (connegEnabled) {
|
|
384
386
|
const contentStr = content.toString();
|
|
385
387
|
const acceptHeader = request.headers.accept || '';
|
|
386
|
-
// Serve Turtle if: URL ends with .ttl OR Accept
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
388
|
+
// Serve Turtle if: URL ends with .ttl OR Accept's q-weighted top
|
|
389
|
+
// RDF type is Turtle/N3 (#325 — naive substring matching ignored
|
|
390
|
+
// q-weights and would pick Turtle whenever it appeared in Accept).
|
|
391
|
+
const negotiated = selectContentType(acceptHeader, true);
|
|
392
|
+
const wantsTurtle = urlPath.endsWith('.ttl')
|
|
393
|
+
|| negotiated === RDF_TYPES.TURTLE
|
|
394
|
+
|| negotiated === RDF_TYPES.N3
|
|
395
|
+
|| negotiated === 'application/n-triples';
|
|
391
396
|
|
|
392
397
|
// Check if this is HTML with JSON-LD data island
|
|
393
398
|
const isHtmlWithDataIsland = contentStr.trimStart().startsWith('<!DOCTYPE') ||
|
|
@@ -505,24 +510,31 @@ export async function handleHead(request, reply) {
|
|
|
505
510
|
let contentType;
|
|
506
511
|
|
|
507
512
|
if (stats.isDirectory) {
|
|
508
|
-
// For directories with index.html, determine content type based on Accept header
|
|
509
513
|
const indexPath = storagePath.endsWith('/') ? `${storagePath}index.html` : `${storagePath}/index.html`;
|
|
510
514
|
const indexExists = await storage.exists(indexPath);
|
|
515
|
+
const acceptHeader = request.headers.accept || '';
|
|
511
516
|
|
|
512
|
-
if (
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
517
|
+
if (connegEnabled) {
|
|
518
|
+
// HEAD must mirror what GET would emit; otherwise client caches and
|
|
519
|
+
// RDF-aware tooling key off a content-type that doesn't match the
|
|
520
|
+
// body they'll see on the next GET (#325). Use q-aware Accept
|
|
521
|
+
// parsing for both the index.html and listing branches.
|
|
522
|
+
const negotiated = selectContentType(acceptHeader, true);
|
|
523
|
+
const wantsTurtle = negotiated === RDF_TYPES.TURTLE
|
|
524
|
+
|| negotiated === RDF_TYPES.N3
|
|
525
|
+
|| negotiated === 'application/n-triples';
|
|
526
|
+
const wantsJsonLd = negotiated === RDF_TYPES.JSON_LD;
|
|
519
527
|
|
|
520
528
|
if (wantsTurtle) {
|
|
521
529
|
contentType = 'text/turtle';
|
|
522
530
|
} else if (wantsJsonLd) {
|
|
523
|
-
|
|
531
|
+
// For an index.html container, only override to JSON-LD if the
|
|
532
|
+
// Accept header explicitly asked for JSON; otherwise fall back
|
|
533
|
+
// to text/html so HEAD matches the index.html that GET serves.
|
|
534
|
+
const explicitJson = /\b(application\/ld\+json|application\/json)\b/i.test(acceptHeader);
|
|
535
|
+
contentType = (indexExists && !explicitJson) ? 'text/html' : 'application/ld+json';
|
|
524
536
|
} else {
|
|
525
|
-
contentType = 'text/html';
|
|
537
|
+
contentType = indexExists ? 'text/html' : 'application/ld+json';
|
|
526
538
|
}
|
|
527
539
|
} else if (indexExists) {
|
|
528
540
|
contentType = 'text/html';
|
package/test/conneg.test.js
CHANGED
|
@@ -354,3 +354,95 @@ describe('Content Negotiation (conneg disabled - default)', () => {
|
|
|
354
354
|
});
|
|
355
355
|
});
|
|
356
356
|
});
|
|
357
|
+
|
|
358
|
+
// Regression coverage for #325 — q-weighted Accept and HEAD/GET parity.
|
|
359
|
+
// Previously the conneg dispatcher used naive substring matching on the
|
|
360
|
+
// Accept header, so any Accept that mentioned text/turtle (even at q=0.1
|
|
361
|
+
// alongside q=1.0 application/ld+json) returned Turtle. Separately, HEAD
|
|
362
|
+
// on a container without an index.html hard-coded application/ld+json,
|
|
363
|
+
// so HEAD and GET disagreed on content-type for the same URL.
|
|
364
|
+
describe('Content Negotiation — q-weights and HEAD/GET parity (#325)', () => {
|
|
365
|
+
before(async () => {
|
|
366
|
+
await startTestServer({ conneg: true });
|
|
367
|
+
await createTestPod('qwtest');
|
|
368
|
+
});
|
|
369
|
+
after(async () => { await stopTestServer(); });
|
|
370
|
+
|
|
371
|
+
function ct(res) {
|
|
372
|
+
return (res.headers.get('content-type') || '').split(';')[0].trim();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
describe('container — q-weight respected', () => {
|
|
376
|
+
it('Accept: jsonld q=1.0, turtle q=0.1 → JSON-LD', async () => {
|
|
377
|
+
const res = await request('/qwtest/', {
|
|
378
|
+
headers: { Accept: 'application/ld+json;q=1.0, text/turtle;q=0.1' }
|
|
379
|
+
});
|
|
380
|
+
assertStatus(res, 200);
|
|
381
|
+
assert.strictEqual(ct(res), 'application/ld+json');
|
|
382
|
+
const body = await res.text();
|
|
383
|
+
assert.ok(body.trimStart().startsWith('{'),
|
|
384
|
+
`body should be JSON, got: ${body.slice(0, 80)}`);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('Accept: jsonld, turtle;q=0.5 → JSON-LD wins (downstream repro)', async () => {
|
|
388
|
+
const res = await request('/qwtest/', {
|
|
389
|
+
headers: { Accept: 'application/ld+json, text/turtle;q=0.5' }
|
|
390
|
+
});
|
|
391
|
+
assert.strictEqual(ct(res), 'application/ld+json');
|
|
392
|
+
const body = await res.text();
|
|
393
|
+
assert.ok(body.trimStart().startsWith('{'),
|
|
394
|
+
`body should be JSON, got: ${body.slice(0, 80)}`);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('Accept: turtle (explicit) → Turtle', async () => {
|
|
398
|
+
const res = await request('/qwtest/', { headers: { Accept: 'text/turtle' } });
|
|
399
|
+
assert.strictEqual(ct(res), 'text/turtle');
|
|
400
|
+
const body = await res.text();
|
|
401
|
+
assert.ok(body.trimStart().startsWith('@prefix'),
|
|
402
|
+
`body should be Turtle, got: ${body.slice(0, 80)}`);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('no Accept → JSON-LD (native default)', async () => {
|
|
406
|
+
const res = await request('/qwtest/');
|
|
407
|
+
assert.strictEqual(ct(res), 'application/ld+json');
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('container — HEAD content-type matches GET', () => {
|
|
412
|
+
const cases = [
|
|
413
|
+
['no Accept', {}],
|
|
414
|
+
['jsonld preferred', { Accept: 'application/ld+json;q=1.0, text/turtle;q=0.1' }],
|
|
415
|
+
['turtle preferred', { Accept: 'text/turtle' }],
|
|
416
|
+
['mixed (q=0.5)', { Accept: 'application/ld+json, text/turtle;q=0.5' }]
|
|
417
|
+
];
|
|
418
|
+
for (const [label, headers] of cases) {
|
|
419
|
+
it(`HEAD === GET content-type — ${label}`, async () => {
|
|
420
|
+
const get = await request('/qwtest/', { headers });
|
|
421
|
+
const head = await request('/qwtest/', { method: 'HEAD', headers });
|
|
422
|
+
assert.strictEqual(get.status, 200);
|
|
423
|
+
assert.strictEqual(head.status, 200);
|
|
424
|
+
assert.strictEqual(ct(head), ct(get),
|
|
425
|
+
`HEAD ct (${ct(head)}) must equal GET ct (${ct(get)}) for ${label}`);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('container — auth path matches anonymous', () => {
|
|
431
|
+
it('GET with auth returns same content-type as without auth (turtle case)', async () => {
|
|
432
|
+
const headers = { Accept: 'text/turtle' };
|
|
433
|
+
const anon = await request('/qwtest/', { headers });
|
|
434
|
+
const authed = await request('/qwtest/', { headers, auth: 'qwtest' });
|
|
435
|
+
assert.strictEqual(ct(anon), 'text/turtle');
|
|
436
|
+
assert.strictEqual(ct(authed), ct(anon),
|
|
437
|
+
'authenticated GET must report the same content-type as anonymous');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('GET with auth returns same content-type as without auth (jsonld case)', async () => {
|
|
441
|
+
const headers = { Accept: 'application/ld+json;q=1.0, text/turtle;q=0.1' };
|
|
442
|
+
const anon = await request('/qwtest/', { headers });
|
|
443
|
+
const authed = await request('/qwtest/', { headers, auth: 'qwtest' });
|
|
444
|
+
assert.strictEqual(ct(anon), 'application/ld+json');
|
|
445
|
+
assert.strictEqual(ct(authed), ct(anon));
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
});
|