javascript-solid-server 0.0.180 → 0.0.181

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.180",
3
+ "version": "0.0.181",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -28,6 +28,14 @@ const LIVE_RELOAD_SCRIPT = `<script>(function(){var ws=new WebSocket((location.p
28
28
  // where a cached data variant was served on top-level navigation (#315).
29
29
  const RDF_CACHE_CONTROL = 'private, no-cache, must-revalidate';
30
30
 
31
+ // Detects when the request's Accept header explicitly names a JSON
32
+ // media type. Used by the container/index.html branches of GET and HEAD
33
+ // to decide whether to surface the embedded JSON-LD data island —
34
+ // without this guard, selectContentType's `*/*` arm would divert plain
35
+ // browser requests into the RDF branch (#409). Hoisted so GET and HEAD
36
+ // can't drift apart silently.
37
+ const EXPLICIT_JSON_RE = /\b(application\/ld\+json|application\/json)\b/i;
38
+
31
39
  /**
32
40
  * Inject live reload script into HTML content
33
41
  */
@@ -161,7 +169,16 @@ export async function handleGet(request, reply) {
161
169
  const wantsTurtle = negotiated === RDF_TYPES.TURTLE
162
170
  || negotiated === RDF_TYPES.N3
163
171
  || negotiated === 'application/n-triples';
164
- const wantsJsonLd = negotiated === RDF_TYPES.JSON_LD;
172
+ // Only treat as JSON-LD when Accept *explicitly* asks for JSON.
173
+ // selectContentType doesn't recognize text/html or
174
+ // application/xhtml+xml, so for a browser Accept like
175
+ // `text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8`
176
+ // it walks past those unsupported types and lands on `*/*`, which
177
+ // returns JSON-LD — diverting plain browser GETs into the RDF
178
+ // branch and serving the embedded data island instead of the
179
+ // index.html body. Mirrors the HEAD-handler logic below (#409).
180
+ const explicitJson = EXPLICIT_JSON_RE.test(acceptHeader);
181
+ const wantsJsonLd = negotiated === RDF_TYPES.JSON_LD && explicitJson;
165
182
 
166
183
  if (wantsTurtle || wantsJsonLd) {
167
184
  // Extract JSON-LD from HTML data island
@@ -601,7 +618,7 @@ export async function handleHead(request, reply) {
601
618
  // For an index.html container, only override to JSON-LD if the
602
619
  // Accept header explicitly asked for JSON; otherwise fall back
603
620
  // to text/html so HEAD matches the index.html that GET serves.
604
- const explicitJson = /\b(application\/ld\+json|application\/json)\b/i.test(acceptHeader);
621
+ const explicitJson = EXPLICIT_JSON_RE.test(acceptHeader);
605
622
  contentType = (indexExists && !explicitJson) ? 'text/html' : 'application/ld+json';
606
623
  } else {
607
624
  contentType = indexExists ? 'text/html' : 'application/ld+json';
@@ -677,4 +677,86 @@ describe('Content Negotiation — q-weights and HEAD/GET parity (#325)', () => {
677
677
  assert.strictEqual(ct(authed), ct(anon));
678
678
  });
679
679
  });
680
+
681
+ describe('container with index.html — browser Accept (#409)', () => {
682
+ // Regression: a container that has an index.html with a *valid*
683
+ // <script type="application/ld+json"> data island used to return that
684
+ // data island as application/ld+json to plain browser GETs.
685
+ // selectContentType iterates the Accept list — for a browser sending
686
+ // `Accept: text/html, ..., */*;q=0.8` it sees text/html (and other
687
+ // HTML-ish types) but doesn't recognize any of them, then hits the
688
+ // `*/*` arm and returns JSON-LD, so the user-visible page silently
689
+ // flipped to JSON.
690
+ const HTML_WITH_JSONLD = '<!DOCTYPE html><html><head><title>Home</title>'
691
+ + '<script type="application/ld+json">'
692
+ + JSON.stringify({ '@context': { foaf: 'http://xmlns.com/foaf/0.1/' }, '@id': '#me', 'foaf:name': 'Carol' })
693
+ + '</script></head><body><h1>hello</h1></body></html>';
694
+
695
+ before(async () => {
696
+ // Container with an index.html containing a parseable JSON-LD island.
697
+ await request('/qwtest/public/page/', { method: 'PUT', auth: 'qwtest' });
698
+ await request('/qwtest/public/page/index.html', {
699
+ method: 'PUT',
700
+ headers: { 'Content-Type': 'text/html' },
701
+ body: HTML_WITH_JSONLD,
702
+ auth: 'qwtest'
703
+ });
704
+ });
705
+
706
+ it('browser Accept (text/html with */*;q=0.8) → text/html, not JSON-LD', async () => {
707
+ const res = await request('/qwtest/public/page/', {
708
+ headers: { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' }
709
+ });
710
+ assertStatus(res, 200);
711
+ assert.strictEqual(ct(res), 'text/html',
712
+ 'browser GET on a container with index.html must return the HTML body, not the embedded data island');
713
+ const body = await res.text();
714
+ assert.ok(body.includes('<h1>hello</h1>'), 'response should be the index.html body');
715
+ });
716
+
717
+ it('plain Accept: text/html → text/html', async () => {
718
+ const res = await request('/qwtest/public/page/', { headers: { Accept: 'text/html' } });
719
+ assertStatus(res, 200);
720
+ assert.strictEqual(ct(res), 'text/html');
721
+ });
722
+
723
+ it('explicit Accept: application/ld+json → JSON-LD from data island still works', async () => {
724
+ const res = await request('/qwtest/public/page/', {
725
+ headers: { Accept: 'application/ld+json' }
726
+ });
727
+ assertStatus(res, 200);
728
+ assert.strictEqual(ct(res), 'application/ld+json');
729
+ const body = await res.json();
730
+ assert.strictEqual(body['foaf:name'], 'Carol',
731
+ 'should still extract the data island when JSON-LD is explicitly asked for');
732
+ });
733
+
734
+ it('explicit Accept: text/turtle → Turtle from data island still works', async () => {
735
+ const res = await request('/qwtest/public/page/', { headers: { Accept: 'text/turtle' } });
736
+ assertStatus(res, 200);
737
+ assert.strictEqual(ct(res), 'text/turtle');
738
+ const body = await res.text();
739
+ assert.ok(body.includes('Carol'), 'turtle output should contain the data island content');
740
+ });
741
+
742
+ // The original bug was specifically GET vs HEAD divergence — the HEAD
743
+ // handler already had the explicitJson guard, GET didn't. Pin the
744
+ // parity here so any future drift between the two branches fails.
745
+ const parityCases = [
746
+ ['browser Accept', { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' }],
747
+ ['Accept: text/html', { Accept: 'text/html' }],
748
+ ['Accept: application/ld+json', { Accept: 'application/ld+json' }],
749
+ ['Accept: text/turtle', { Accept: 'text/turtle' }]
750
+ ];
751
+ for (const [label, headers] of parityCases) {
752
+ it(`HEAD === GET content-type — ${label}`, async () => {
753
+ const get = await request('/qwtest/public/page/', { headers });
754
+ const head = await request('/qwtest/public/page/', { method: 'HEAD', headers });
755
+ assert.strictEqual(get.status, 200);
756
+ assert.strictEqual(head.status, 200);
757
+ assert.strictEqual(ct(head), ct(get),
758
+ `HEAD ct (${ct(head)}) must equal GET ct (${ct(get)}) for ${label}`);
759
+ });
760
+ }
761
+ });
680
762
  });