Skip to content

Commit 3452a59

Browse files
committed
it lints!
1 parent 1947364 commit 3452a59

File tree

6 files changed

+209
-155
lines changed

6 files changed

+209
-155
lines changed

package-lock.json

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
},
6363
"homepage": "https://github.com/express-rate-limit/ratelimit-header-parser#readme",
6464
"devDependencies": {
65+
"@express-rate-limit/prettier": "^1.0.0",
66+
"@express-rate-limit/tsconfig": "^1.0.0",
67+
"@jest/globals": "^29.6.3",
6568
"@jest/types": "^29.6.3",
6669
"@types/node": "^20.5.1",
6770
"del-cli": "^5.0.0",
@@ -96,8 +99,27 @@
9699
"^(\\.{1,2}/.*)\\.js$": "$1"
97100
}
98101
},
99-
"dependencies": {
100-
"@express-rate-limit/prettier": "^1.0.0",
101-
"@express-rate-limit/tsconfig": "^1.0.0"
102+
"xo": {
103+
"prettier": true,
104+
"rules": {
105+
"@typescript-eslint/consistent-indexed-object-style": [
106+
"error",
107+
"index-signature"
108+
],
109+
"n/no-unsupported-features/es-syntax": 0
110+
},
111+
"overrides": [
112+
{
113+
"files": "test/*.ts",
114+
"rules": {
115+
"@typescript-eslint/no-unsafe-argument": 0
116+
}
117+
}
118+
]
119+
},
120+
"prettier": "@express-rate-limit/prettier",
121+
"lint-staged": {
122+
"{source,test}/**/*.ts": "xo --fix",
123+
"**/*.{json,yaml,md}": "prettier --ignore-path .gitignore --ignore-unknown --write "
102124
}
103125
}

source/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export * from './types'
2-
export { parseRateLimit } from './ratelimit-header-parser'
1+
export * from './types.js'
2+
export { parseRateLimit } from './ratelimit-header-parser.js'

source/ratelimit-header-parser.ts

Lines changed: 138 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,170 @@
1-
import type { ServerResponse, IncomingHttpHeaders, OutgoingHttpHeaders } from 'node:http'
21
import type {
3-
RateLimit,
4-
RateLimitOptions,
5-
} from './types'
6-
7-
// node or fetch
8-
export type ResponseObject = ServerResponse | Response;
9-
export type HeadersObject = IncomingHttpHeaders | OutgoingHttpHeaders | Headers | Object;
10-
export type ResponseOrHeadersObject = ResponseObject | HeadersObject;
11-
12-
export function parseRateLimit(input: ResponseOrHeadersObject, options?: RateLimitOptions): RateLimit | undefined {
13-
if ('headers' in input && typeof input.headers === 'object' && !Array.isArray(input.headers)) {
14-
return parseHeadersObject(input.headers, options)
15-
} else if ('getHeaders' in input && typeof input.getHeaders === 'function') {
16-
return parseHeadersObject(input.getHeaders(), options)
17-
} else {
18-
return parseHeadersObject(input, options)
19-
}
2+
ServerResponse,
3+
IncomingHttpHeaders,
4+
OutgoingHttpHeaders,
5+
} from 'node:http'
6+
import type { RateLimit, RateLimitOptions } from './types'
7+
8+
// Node or fetch
9+
export type ResponseObject = ServerResponse | Response
10+
export type HeadersObject =
11+
| IncomingHttpHeaders
12+
| OutgoingHttpHeaders
13+
| Headers
14+
| { [key: string]: unknown }
15+
export type ResponseOrHeadersObject = ResponseObject | HeadersObject
16+
17+
export function parseRateLimit(
18+
input: ResponseOrHeadersObject,
19+
options?: RateLimitOptions,
20+
): RateLimit | undefined {
21+
if (
22+
'headers' in input &&
23+
typeof input.headers === 'object' &&
24+
!Array.isArray(input.headers)
25+
) {
26+
return parseHeadersObject(input.headers, options)
27+
}
28+
29+
if ('getHeaders' in input && typeof input.getHeaders === 'function') {
30+
return parseHeadersObject(input.getHeaders(), options)
31+
}
32+
33+
return parseHeadersObject(input, options)
2034
}
2135

22-
function parseHeadersObject(input: HeadersObject, options: RateLimitOptions | undefined): RateLimit | undefined {
23-
let combined = getHeader(input, 'ratelimit')
24-
if (combined ) return parseCombinedRateLimitHeader(combined);
25-
26-
let prefix;
27-
if (getHeader(input, 'ratelimit-remaining')) {
28-
prefix = 'ratelimit-'
29-
} else if (getHeader(input, 'x-ratelimit-remaining')) {
30-
prefix = 'x-ratelimit-'
31-
} else if (getHeader(input, 'x-rate-limit-remaining')) {
32-
// twitter - https://developer.twitter.com/en/docs/twitter-api/rate-limits#headers-and-codes
33-
prefix = 'x-rate-limit-'
34-
} else {
35-
// todo: handle other vendor-specific headers - see
36-
// https://github.com/ietf-wg-httpapi/ratelimit-headers/issues/25
37-
// https://stackoverflow.com/questions/16022624/examples-of-http-api-rate-limiting-http-response-headers
38-
// https://github.com/mre/rate-limits/blob/master/src/variants.rs
39-
// etc.
40-
return;
41-
}
42-
43-
const limit = num(getHeader(input, `${prefix}limit`))
44-
// used - https://github.com/reddit-archive/reddit/wiki/API#rules
45-
// used - https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limit-headers
46-
// observed - https://docs.gitlab.com/ee/administration/settings/user_and_ip_rate_limits.html#response-headers
47-
// note that || is valid here because used should always be at least 1, and || handles NaN correctly, whereas ?? doesn't
48-
const used = num(getHeader(input, `${prefix}used`)) || num(getHeader(input, `${prefix}observed`))
49-
const remaining = num(getHeader(input, `${prefix}remaining`))
50-
51-
let reset: Date|undefined = undefined;
52-
const resetRaw = getHeader(input, `${prefix}reset`)
53-
const resetType = options?.reset;
54-
if (resetType == 'date') reset = parseResetDate(resetRaw ?? '');
55-
else if (resetType == 'unix') reset = parseResetUnix(resetRaw ?? '');
56-
else if (resetType == 'seconds') reset = parseResetSeconds(resetRaw ?? '');
57-
else if (resetType == 'milliseconds') reset = parseResetMilliseconds(resetRaw ?? '');
58-
else if (resetRaw) reset = parseResetAuto(resetRaw)
59-
else {
60-
// fallback to retry-after
61-
const retryAfter = getHeader(input, 'retry-after');
62-
if (retryAfter) {
63-
reset = parseResetUnix(retryAfter)
64-
}
65-
}
66-
67-
return {
68-
limit: isNaN(limit) ? used + remaining : limit, // reddit omits
69-
used: isNaN(used) ? limit - remaining : used, // most omit
70-
remaining,
71-
reset
72-
}
36+
function parseHeadersObject(
37+
input: HeadersObject,
38+
options: RateLimitOptions | undefined,
39+
): RateLimit | undefined {
40+
const combined = getHeader(input, 'ratelimit')
41+
if (combined) return parseCombinedRateLimitHeader(combined)
42+
43+
let prefix
44+
if (getHeader(input, 'ratelimit-remaining')) {
45+
prefix = 'ratelimit-'
46+
} else if (getHeader(input, 'x-ratelimit-remaining')) {
47+
prefix = 'x-ratelimit-'
48+
} else if (getHeader(input, 'x-rate-limit-remaining')) {
49+
// Twitter - https://developer.twitter.com/en/docs/twitter-api/rate-limits#headers-and-codes
50+
prefix = 'x-rate-limit-'
51+
} else {
52+
// Todo: handle other vendor-specific headers - see
53+
// https://github.com/ietf-wg-httpapi/ratelimit-headers/issues/25
54+
// https://stackoverflow.com/questions/16022624/examples-of-http-api-rate-limiting-http-response-headers
55+
// https://github.com/mre/rate-limits/blob/master/src/variants.rs
56+
// etc.
57+
return
58+
}
59+
60+
const limit = number_(getHeader(input, `${prefix}limit`))
61+
// Used - https://github.com/reddit-archive/reddit/wiki/API#rules
62+
// used - https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limit-headers
63+
// observed - https://docs.gitlab.com/ee/administration/settings/user_and_ip_rate_limits.html#response-headers
64+
// note that || is valid here because used should always be at least 1, and || handles NaN correctly, whereas ?? doesn't
65+
const used =
66+
number_(getHeader(input, `${prefix}used`)) ||
67+
number_(getHeader(input, `${prefix}observed`))
68+
const remaining = number_(getHeader(input, `${prefix}remaining`))
69+
70+
let reset: Date | undefined
71+
const resetRaw = getHeader(input, `${prefix}reset`)
72+
const resetType = options?.reset
73+
if (resetType == 'date') reset = parseResetDate(resetRaw ?? '')
74+
else if (resetType == 'unix') reset = parseResetUnix(resetRaw ?? '')
75+
else if (resetType == 'seconds') reset = parseResetSeconds(resetRaw ?? '')
76+
else if (resetType == 'milliseconds')
77+
reset = parseResetMilliseconds(resetRaw ?? '')
78+
else if (resetRaw) reset = parseResetAuto(resetRaw)
79+
else {
80+
// Fallback to retry-after
81+
const retryAfter = getHeader(input, 'retry-after')
82+
if (retryAfter) {
83+
reset = parseResetUnix(retryAfter)
84+
}
85+
}
86+
87+
return {
88+
limit: isNaN(limit) ? used + remaining : limit, // Reddit omits
89+
used: isNaN(used) ? limit - remaining : used, // Most omit
90+
remaining,
91+
reset,
92+
}
7393
}
7494

7595
const reLimit = /limit\s*=\s*(\d+)/i
7696
const reRemaining = /remaining\s*=\s*(\d+)/i
7797
const reReset = /reset\s*=\s*(\d+)/i
7898
export function parseCombinedRateLimitHeader(input: string): RateLimit {
79-
const limit = num(reLimit.exec(input)?.[1]);
80-
const remaining = num(reRemaining.exec(input)?.[1]);
81-
const resetSeconds = num(reReset.exec(input)?.[1]);
82-
const reset = secondsToDate(resetSeconds);
83-
return {
84-
limit,
85-
used: limit - remaining,
86-
remaining,
87-
reset,
88-
}
99+
const limit = number_(reLimit.exec(input)?.[1])
100+
const remaining = number_(reRemaining.exec(input)?.[1])
101+
const resetSeconds = number_(reReset.exec(input)?.[1])
102+
const reset = secondsToDate(resetSeconds)
103+
return {
104+
limit,
105+
used: limit - remaining,
106+
remaining,
107+
reset,
108+
}
89109
}
90110

91111
function secondsToDate(seconds: number): Date {
92-
const d = new Date();
93-
d.setSeconds(d.getSeconds() + seconds);
94-
return d;
112+
const d = new Date()
113+
d.setSeconds(d.getSeconds() + seconds)
114+
return d
95115
}
96116

97-
function num(input: string | number | undefined): number {
98-
if (typeof input == 'number') return input;
99-
return parseInt(input ?? '', 10);
117+
function number_(input: string | number | undefined): number {
118+
if (typeof input === 'number') return input
119+
return Number.parseInt(input ?? '', 10)
100120
}
101121

102122
function getHeader(headers: HeadersObject, name: string): string | undefined {
103-
if ('get' in headers && typeof headers.get === 'function') {
104-
return headers.get(name) ?? undefined // returns null if missing, but everything else is undefined for missing values
105-
} else if (name in headers && typeof (headers as any)[name] == 'string') {
106-
return (headers as any)[name]
107-
}
108-
return undefined
123+
if ('get' in headers && typeof headers.get === 'function') {
124+
return headers.get(name) ?? undefined // Returns null if missing, but everything else is undefined for missing values
125+
}
126+
127+
if (name in headers && typeof (headers as any)[name] === 'string') {
128+
return (headers as any)[name]
129+
}
130+
131+
return undefined
109132
}
110133

111134
function parseResetDate(resetRaw: string): Date {
112-
// todo: take the server's date into account, calculate an offset, then apply that to the current date
113-
return new Date(resetRaw);
135+
// Todo: take the server's date into account, calculate an offset, then apply that to the current date
136+
return new Date(resetRaw)
114137
}
115138

116139
function parseResetUnix(resetRaw: string | number): Date {
117-
let resetNum = num(resetRaw);
118-
return new Date(resetNum * 1000);
140+
const resetNumber = number_(resetRaw)
141+
return new Date(resetNumber * 1000)
119142
}
120143

121144
function parseResetSeconds(resetRaw: string | number): Date {
122-
let resetNum = num(resetRaw);
123-
return secondsToDate(resetNum);
145+
const resetNumber = number_(resetRaw)
146+
return secondsToDate(resetNumber)
124147
}
125148

126149
function parseResetMilliseconds(resetRaw: string | number): Date {
127-
let resetNum = num(resetRaw);
128-
return secondsToDate(resetNum/1000);
150+
const resetNumber = number_(resetRaw)
151+
return secondsToDate(resetNumber / 1000)
129152
}
130153

131-
const reLetters = /[a-z]/i;
154+
const reLetters = /[a-z]/i
132155
function parseResetAuto(resetRaw: string): Date {
133-
// if it has any letters, assume it's a date string
134-
if (resetRaw.match(reLetters)) {
135-
return parseResetDate(resetRaw)
136-
}
137-
const resetNum = num(resetRaw);
138-
// looks like a unix timestamp
139-
if (resetNum && resetNum > 1000000000) { // sometime in 2001
140-
return parseResetUnix(resetNum)
141-
}
142-
// could be seconds or milliseconds (or something else!), defaulting to seconds
143-
return parseResetSeconds(resetNum);
144-
}
156+
// If it has any letters, assume it's a date string
157+
if (reLetters.test(resetRaw)) {
158+
return parseResetDate(resetRaw)
159+
}
160+
161+
const resetNumber = number_(resetRaw)
162+
// Looks like a unix timestamp
163+
if (resetNumber && resetNumber > 1_000_000_000) {
164+
// Sometime in 2001
165+
return parseResetUnix(resetNumber)
166+
}
167+
168+
// Could be seconds or milliseconds (or something else!), defaulting to seconds
169+
return parseResetSeconds(resetNumber)
170+
}

source/types.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
export type RateLimit = {
2-
limit: number,
3-
used: number,
4-
remaining: number,
5-
reset?: Date,
6-
// todo: policy
2+
limit: number
3+
used: number
4+
remaining: number
5+
reset?: Date
6+
// Todo: policy
77
}
88

99
export type RateLimitOptions = {
10-
reset?: 'date' | 'unix' | 'seconds' | 'milliseconds',
10+
reset?: 'date' | 'unix' | 'seconds' | 'milliseconds'
1111
}

0 commit comments

Comments
 (0)