1
- import type { ServerResponse , IncomingHttpHeaders , OutgoingHttpHeaders } from 'node:http'
2
1
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 )
20
34
}
21
35
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
+ }
73
93
}
74
94
75
95
const reLimit = / l i m i t \s * = \s * ( \d + ) / i
76
96
const reRemaining = / r e m a i n i n g \s * = \s * ( \d + ) / i
77
97
const reReset = / r e s e t \s * = \s * ( \d + ) / i
78
98
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
+ }
89
109
}
90
110
91
111
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
95
115
}
96
116
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 )
100
120
}
101
121
102
122
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
109
132
}
110
133
111
134
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 )
114
137
}
115
138
116
139
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 )
119
142
}
120
143
121
144
function parseResetSeconds ( resetRaw : string | number ) : Date {
122
- let resetNum = num ( resetRaw ) ;
123
- return secondsToDate ( resetNum ) ;
145
+ const resetNumber = number_ ( resetRaw )
146
+ return secondsToDate ( resetNumber )
124
147
}
125
148
126
149
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 )
129
152
}
130
153
131
- const reLetters = / [ a - z ] / i;
154
+ const reLetters = / [ a - z ] / i
132
155
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
+ }
0 commit comments