-
-
Notifications
You must be signed in to change notification settings - Fork 660
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add language detector middleware and helpers #3787
Merged
+834
−27
Merged
Changes from 1 commit
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
fcaa8c2
feat(language-detector): add language detector middleware and helper …
lord007tn 629eb03
chore(language-detector): add export in package.json
lord007tn bdf1a30
chore(language-detector): add export to jsr
lord007tn 05007db
feat: new parse-accept helper, add edge case tests
lord007tn 44fe77d
chore: add jsr for parse-accept
lord007tn 6b4e053
fix: export default options, remove empty type
lord007tn e84b840
refac: rename languageDetectorMiddleware to languageDetector
lord007tn 2127d80
chore format code
lord007tn 007ea3d
refac: apply patches
lord007tn ecdbd5e
refactor: change export type in language
lord007tn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: new parse-accept helper, add edge case tests
commit 05007db367f955f75ed2d136508256ba00a23864
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -272,4 +272,5 @@ describe('languageDetector', () => { | |
consoleSpy.mockRestore() | ||
}) | ||
}) | ||
|
||
}) |
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,156 @@ | ||
import { parseAccept } from './parse-accept' | ||
|
||
describe('parseAccept Comprehensive Tests', () => { | ||
describe('Basic Functionality', () => { | ||
test('parses simple accept header', () => { | ||
const header = 'text/html,application/json;q=0.9' | ||
expect(parseAccept(header)).toEqual([ | ||
{ type: 'text/html', params: {}, q: 1 }, | ||
{ type: 'application/json', params: { q: '0.9' }, q: 0.9 }, | ||
]) | ||
}) | ||
|
||
test('handles missing header', () => { | ||
expect(parseAccept('')).toEqual([]) | ||
expect(parseAccept(undefined as any)).toEqual([]) | ||
yusukebe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
expect(parseAccept(null as any)).toEqual([]) | ||
}) | ||
}) | ||
|
||
describe('Quality Values', () => { | ||
test('handles extreme q values', () => { | ||
const header = 'a;q=999999,b;q=-99999,c;q=Infinity,d;q=-Infinity,e;q=NaN' | ||
const result = parseAccept(header) | ||
expect(result.map((x) => x.q)).toEqual([1, 1, 1, 0, 0]) | ||
}) | ||
|
||
test('handles malformed q values', () => { | ||
const header = 'a;q=,b;q=invalid,c;q=1.2.3,d;q=true,e;q="0.5"' | ||
const result = parseAccept(header) | ||
expect(result.every((x) => x.q >= 0 && x.q <= 1)).toBe(true) | ||
}) | ||
|
||
test('preserves original q string in params', () => { | ||
const header = 'type;q=invalid' | ||
const result = parseAccept(header) | ||
expect(result[0].params.q).toBe('invalid') | ||
expect(result[0].q).toBe(1) // Normalized q value | ||
}) | ||
}) | ||
|
||
describe('Parameter Handling', () => { | ||
test('handles complex parameters', () => { | ||
const header = 'type;a=1;b="2";c=\'3\';d="semi;colon";e="nested"quoted""' | ||
const result = parseAccept(header) | ||
expect(result[0].params).toEqual({ | ||
a: '1', | ||
b: '"2"', | ||
// eslint-disable-next-line quotes | ||
c: "'3'", | ||
d: '"semi;colon"', | ||
e: '"nested"quoted""' | ||
}) | ||
}) | ||
|
||
test('handles malformed parameters', () => { | ||
const header = 'type;=value;;key=;=;====;key====value' | ||
const result = parseAccept(header) | ||
expect(result[0].type).toBe('type') | ||
expect(Object.keys(result[0].params).length).toBe(0) | ||
}) | ||
|
||
test('handles duplicate parameters', () => { | ||
const header = 'type;key=1;key=2;KEY=3' | ||
const result = parseAccept(header) | ||
expect(result[0].params.key).toBe('2') | ||
expect(result[0].params.KEY).toBe('3') | ||
}) | ||
}) | ||
|
||
describe('Media Type Edge Cases', () => { | ||
test('handles malformed media types', () => { | ||
const headers = [ | ||
'*/html', | ||
'text/*mal/formed', | ||
'/partial', | ||
'missing/', | ||
'inv@lid/type', | ||
'text/(html)', | ||
'text/html?invalid', | ||
] | ||
headers.forEach((header) => { | ||
const result = parseAccept(header) | ||
expect(result[0].type).toBe(header) | ||
}) | ||
}) | ||
|
||
test('handles extremely long types', () => { | ||
const longType = 'a'.repeat(10000) + '/' + 'b'.repeat(10000) | ||
const result = parseAccept(longType) | ||
expect(result[0].type).toBe(longType) | ||
}) | ||
}) | ||
|
||
describe('Delimiter Edge Cases', () => { | ||
test('handles multiple consecutive delimiters', () => { | ||
const header = 'a,,,,b;q=0.9,,,,c;q=0.8,,,,' | ||
const result = parseAccept(header) | ||
expect(result.map((x) => x.type)).toEqual(['a', 'b', 'c']) | ||
}) | ||
|
||
test('handles unusual whitespace', () => { | ||
const header = '\n\t a \t\n ; \n\t q=0.9 \t\n , \n\t b \t\n' | ||
const result = parseAccept(header) | ||
expect(result.map((x) => x.type)).toEqual(['b', 'a']) | ||
}) | ||
}) | ||
|
||
describe('Security Cases', () => { | ||
test('handles potential injection patterns', () => { | ||
const headers = [ | ||
'type;q=0.9--', | ||
'type;q=0.9;drop table users', | ||
'type;__|;q=0.9', | ||
'text/html"><script>alert(1)</script>', | ||
'application/json${process.env}', | ||
] | ||
headers.forEach((header) => { | ||
expect(() => parseAccept(header)).not.toThrow() | ||
}) | ||
}) | ||
|
||
test('handles extremely large input', () => { | ||
const header = 'a;q=0.9,'.repeat(100000) | ||
expect(() => parseAccept(header)).not.toThrow() | ||
}) | ||
}) | ||
|
||
describe('Unicode and Special Characters', () => { | ||
test('handles unicode in types and parameters', () => { | ||
const header = '🌐/😊;param=🔥;q=0.9' | ||
const result = parseAccept(header) | ||
expect(result[0].type).toBe('🌐/😊') | ||
expect(result[0].params.param).toBe('🔥') | ||
}) | ||
|
||
test('handles special characters', () => { | ||
const header = 'type;param=\x00\x01\x02\x03' | ||
const result = parseAccept(header) | ||
expect(result[0].params.param).toBe('\x00\x01\x02\x03') | ||
}) | ||
}) | ||
|
||
describe('Sort Stability', () => { | ||
test('maintains stable sort for equal q values', () => { | ||
const header = 'a;q=0.9,b;q=0.9,c;q=0.9,d;q=0.9' | ||
const result = parseAccept(header) | ||
expect(result.map((x) => x.type)).toEqual(['a', 'b', 'c', 'd']) | ||
}) | ||
|
||
test('handles mixed priorities correctly', () => { | ||
const header = 'd;q=0.8,b;q=0.9,c;q=0.8,a;q=0.9' | ||
const result = parseAccept(header) | ||
expect(result.map((x) => x.type)).toEqual(['b', 'a', 'd', 'c']) | ||
}) | ||
}) | ||
}) |
yusukebe marked this conversation as resolved.
Show resolved
Hide resolved
|
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,86 @@ | ||
export interface Accept { | ||
type: string | ||
params: Record<string, string> | ||
q: number | ||
} | ||
|
||
/** | ||
* Parse an Accept header into an array of objects with type, parameters, and quality score. | ||
* @param acceptHeader The Accept header string | ||
* @returns An array of parsed Accept values | ||
*/ | ||
export const parseAccept = (acceptHeader: string): Accept[] => { | ||
if (!acceptHeader) { | ||
return [] | ||
} | ||
|
||
const acceptValues = acceptHeader.split(',').map((value, index) => ({ value, index })) | ||
|
||
return acceptValues | ||
.map(parseAcceptValue) | ||
.filter((item): item is Accept & { index: number } => Boolean(item)) | ||
.sort(sortByQualityAndIndex) | ||
.map(({ type, params, q }) => ({ type, params, q })) | ||
} | ||
|
||
const parseAcceptValue = ({ value, index }: { value: string; index: number }) => { | ||
const parts = value | ||
.trim() | ||
.split(/;(?=(?:(?:[^"]*"){2})*[^"]*$)/) | ||
.map((s) => s.trim()) | ||
const type = parts[0] | ||
if (!type) { | ||
return null | ||
} | ||
|
||
const params = parseParams(parts.slice(1)) | ||
const q = parseQuality(params.q) | ||
|
||
return { type, params, q, index } | ||
} | ||
yusukebe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const parseParams = (paramParts: string[]): Record<string, string> => { | ||
return paramParts.reduce<Record<string, string>>((acc, param) => { | ||
const [key, val] = param.split('=').map((s) => s.trim()) | ||
if (key && val) { | ||
acc[key] = val | ||
} | ||
return acc | ||
}, {}) | ||
} | ||
|
||
const parseQuality = (qVal?: string): number => { | ||
if (qVal === undefined) { | ||
return 1.0 | ||
} | ||
if (qVal === '') { | ||
return 1 | ||
} | ||
if (qVal === 'NaN') { | ||
return 0 | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q: is there any reason for using "1.0" and "0"? |
||
|
||
const num = Number(qVal) | ||
if (num === Infinity) { | ||
return 1 | ||
} | ||
if (num === -Infinity) { | ||
return 0 | ||
} | ||
if (Number.isNaN(num)) { | ||
return 1 | ||
} | ||
if (num < 0 || num > 1) { | ||
return 1 | ||
} | ||
|
||
return num | ||
} | ||
|
||
const sortByQualityAndIndex = (a: Accept & { index: number }, b: Accept & { index: number }) => { | ||
const qDiff = b.q - a.q | ||
if (qDiff !== 0) { | ||
return qDiff | ||
} | ||
return a.index - b.index | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to export this.