-
-
Notifications
You must be signed in to change notification settings - Fork 291
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support qvalue weighting in Accept headers (#5966)
- Loading branch information
Showing
6 changed files
with
134 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import {ResponseFormat} from "../interfaces.js"; | ||
|
||
enum MediaType { | ||
json = "application/json", | ||
ssz = "application/octet-stream", | ||
} | ||
|
||
const MEDIA_TYPES: { | ||
[K in ResponseFormat]: MediaType; | ||
} = { | ||
json: MediaType.json, | ||
ssz: MediaType.ssz, | ||
}; | ||
|
||
function responseFormatFromMediaType(mediaType: MediaType): ResponseFormat { | ||
switch (mediaType) { | ||
default: | ||
case MediaType.json: | ||
return "json"; | ||
case MediaType.ssz: | ||
return "ssz"; | ||
} | ||
} | ||
|
||
export function writeAcceptHeader(format?: ResponseFormat): MediaType { | ||
return format === undefined ? MEDIA_TYPES["json"] : MEDIA_TYPES[format]; | ||
} | ||
|
||
export function parseAcceptHeader(accept?: string): ResponseFormat { | ||
// Use json by default. | ||
if (!accept) { | ||
return "json"; | ||
} | ||
|
||
const mediaTypes = Object.values(MediaType); | ||
|
||
// Respect Quality Values per RFC-9110 | ||
// Acceptable mime-types are comma separated with optional whitespace | ||
return responseFormatFromMediaType( | ||
accept | ||
.toLowerCase() | ||
.split(",") | ||
.map((x) => x.trim()) | ||
.reduce( | ||
(best: [number, MediaType], current: string): [number, MediaType] => { | ||
// An optional `;` delimiter is used to separate the mime-type from the weight | ||
// Normalize here, using 1 as the default qvalue | ||
const quality = current.includes(";") ? current.split(";") : [current, "q=1"]; | ||
|
||
const mediaType = quality[0].trim() as MediaType; | ||
|
||
// If the mime type isn't acceptable, move on to the next entry | ||
if (!mediaTypes.includes(mediaType)) { | ||
return best; | ||
} | ||
|
||
// Otherwise, the portion after the semicolon has optional whitespace and the constant prefix "q=" | ||
const weight = quality[1].trim(); | ||
if (!weight.startsWith("q=")) { | ||
// If the format is invalid simply move on to the next entry | ||
return best; | ||
} | ||
|
||
const qvalue = +weight.replace("q=", ""); | ||
if (isNaN(qvalue) || qvalue > 1 || qvalue <= 0) { | ||
// If we can't convert the qvalue to a valid number, move on | ||
return best; | ||
} | ||
|
||
if (qvalue < best[0]) { | ||
// This mime type is not preferred | ||
return best; | ||
} | ||
|
||
// This mime type is preferred | ||
return [qvalue, mediaType]; | ||
}, | ||
[0, MediaType.json] | ||
)[1] | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import {expect} from "chai"; | ||
import {parseAcceptHeader} from "../../../src/utils/acceptHeader.js"; | ||
import {ResponseFormat} from "../../../src/interfaces.js"; | ||
|
||
describe("utils / acceptHeader", () => { | ||
describe("parseAcceptHeader", () => { | ||
const testCases: {header: string | undefined; expected: ResponseFormat}[] = [ | ||
{header: undefined, expected: "json"}, | ||
{header: "application/json", expected: "json"}, | ||
{header: "application/octet-stream", expected: "ssz"}, | ||
{header: "application/invalid", expected: "json"}, | ||
{header: "application/invalid;q=1,application/octet-stream;q=0.1", expected: "ssz"}, | ||
{header: "application/octet-stream;q=0.5,application/json;q=1", expected: "json"}, | ||
{header: "application/octet-stream;q=1,application/json;q=0.1", expected: "ssz"}, | ||
{header: "application/octet-stream,application/json;q=0.1", expected: "ssz"}, | ||
{header: "application/octet-stream;,application/json;q=0.1", expected: "json"}, | ||
{header: "application/octet-stream;q=2,application/json;q=0.1", expected: "json"}, | ||
{header: "application/octet-stream;q=invalid,application/json;q=0.1", expected: "json"}, | ||
{header: "application/octet-stream;q=invalid,application/json;q=0.1", expected: "json"}, | ||
{header: "application/octet-stream ; q=0.5 , application/json ; q=1", expected: "json"}, | ||
{header: "application/octet-stream ; q=1 , application/json ; q=0.1", expected: "ssz"}, | ||
{header: "application/octet-stream;q=1,application/json;q=0.1", expected: "ssz"}, | ||
|
||
// The implementation is order dependent, however, RFC-9110 doesn't specify a preference. | ||
// The following tests serve to document the behavior at the time of implementation- not a | ||
// specific requirement from the spec. In this case, last wins. | ||
{header: "application/octet-stream;q=1,application/json;q=1", expected: "json"}, | ||
{header: "application/json;q=1,application/octet-stream;q=1", expected: "ssz"}, | ||
]; | ||
|
||
for (const testCase of testCases) { | ||
it(`should correctly parse the header ${testCase.header}`, () => { | ||
expect(parseAcceptHeader(testCase.header)).to.equal(testCase.expected); | ||
}); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters