Skip to content

Commit 92ebc48

Browse files
[Agent: Claude] fix(conneg): R13 Accept-Profile q robustness — discard q=0, clamp [0,1]
- parseAcceptProfile now clamps q values to [0, 1] range per RFC 9110 - q=0 entries are filtered out (explicitly not acceptable per §12.5.1) - Out-of-range weights (q<0 or q>1) are normalized instead of kept - Non-numeric q parameters default to 1.0 (unchanged behavior) - Added 5 new tests covering all edge cases Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e74a2bb commit 92ebc48

2 files changed

Lines changed: 31 additions & 3 deletions

File tree

src/rdf/conneg.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,14 @@ export function parseAcceptProfile(header) {
160160
const [ref, ...params] = e.split(';').map((s) => s.trim());
161161
const uri = ref.replace(/^</, '').replace(/>$/, '');
162162
const qParam = params.find((p) => p.toLowerCase().startsWith('q='));
163-
const q = qParam ? parseFloat(qParam.slice(2)) : 1.0;
164-
return { uri, q: Number.isFinite(q) ? q : 1.0, i };
165-
}).filter((p) => p.uri);
163+
const qRaw = qParam ? parseFloat(qParam.slice(2)) : 1.0;
164+
// R13 (RFC 9110 robustness): clamp out-of-range weights into [0,1];
165+
// non-numeric falls back to 1.0. q=0 is §12.5.1 "explicitly not
166+
// acceptable" — discarded below, matching this file's media-type
167+
// consumers (acceptSatisfiable/acceptsHtml).
168+
const q = Number.isFinite(qRaw) ? Math.min(Math.max(qRaw, 0), 1) : 1.0;
169+
return { uri, q, i };
170+
}).filter((p) => p.uri && p.q !== 0);
166171
parsed.sort((a, b) => (b.q - a.q) || (a.i - b.i));
167172
return parsed.map((p) => p.uri);
168173
}

test/conneg-accept-profile.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,26 @@ test('parseAcceptProfile: multiple, ordered by q desc, stable ties', () => {
1717
['https://ex.org/p/y', 'https://ex.org/p/x']
1818
);
1919
});
20+
21+
test('R13: q=0 entry is discarded (RFC 9110 §12.5.1 "not acceptable")', () => {
22+
assert.deepEqual(parseAcceptProfile('<https://p/a>;q=0, <https://p/b>;q=0.5'), ['https://p/b']);
23+
});
24+
25+
test('R13: all-q=0 header yields empty list (degrades to outcome none)', () => {
26+
assert.deepEqual(parseAcceptProfile('<https://p/a>;q=0'), []);
27+
});
28+
29+
test('R13: out-of-range q clamps into [0,1] — q=2 no longer outranks q=1', () => {
30+
assert.deepEqual(parseAcceptProfile('<https://p/a>;q=2, <https://p/b>'), ['https://p/a', 'https://p/b']);
31+
// clamped to 1.0 each; stable input order breaks the tie — b would LOSE its
32+
// rightful tie if 2.0 were kept (it used to sort strictly above q=1)
33+
assert.deepEqual(parseAcceptProfile('<https://p/a>, <https://p/b>;q=2'), ['https://p/a', 'https://p/b']);
34+
});
35+
36+
test('R13: negative q clamps to 0 and is discarded', () => {
37+
assert.deepEqual(parseAcceptProfile('<https://p/a>;q=-1, <https://p/b>'), ['https://p/b']);
38+
});
39+
40+
test('R13: non-numeric q stays 1.0 (unchanged behavior, now pinned)', () => {
41+
assert.deepEqual(parseAcceptProfile('<https://p/a>;q=abc, <https://p/b>;q=0.5'), ['https://p/a', 'https://p/b']);
42+
});

0 commit comments

Comments
 (0)