@@ -8,31 +8,72 @@ export type RateLimit = {
8
8
// todo: policy
9
9
}
10
10
11
- type ResponseOrHeadersObject =
12
- /* Node.js things */
13
- ServerResponse | IncomingHttpHeaders | OutgoingHttpHeaders |
14
- /* fetch things */
15
- Response | Headers ;
11
+ export type RateLimitOptions = {
12
+ reset ?: 'date' | 'unix' | 'seconds' | 'milliseconds' ,
13
+ }
14
+
15
+ // node or fetch
16
+ type ResponseObject = ServerResponse | Response ;
17
+ type HeadersObject = IncomingHttpHeaders | OutgoingHttpHeaders | Headers | Object ;
16
18
17
- export function parseRateLimitHeaders ( input : ResponseOrHeadersObject ) : RateLimit | undefined {
19
+ export function parseRateLimit ( input : ResponseObject | HeadersObject , options ?: RateLimitOptions ) : RateLimit | undefined {
18
20
if ( 'headers' in input && typeof input . headers === 'object' && ! Array . isArray ( input . headers ) ) {
19
- return parseRateLimitHeaders ( input . headers )
21
+ return parseRateLimit ( input . headers , options )
20
22
} else if ( 'getHeaders' in input && typeof input . getHeaders === 'function' ) {
21
- return parseRateLimitHeaders ( input . getHeaders ( ) )
23
+ return parseRateLimit ( input . getHeaders ( ) , options )
24
+ } else {
25
+ return parseHeadersObject ( input , options )
26
+ }
27
+ }
28
+
29
+ function parseHeadersObject ( input : HeadersObject , options : RateLimitOptions | undefined ) : RateLimit | undefined {
30
+ let combined = getHeader ( input , 'ratelimit' )
31
+ if ( combined ) return parseCombinedRateLimitHeader ( combined ) ;
32
+
33
+ let prefix ;
34
+ if ( getHeader ( input , 'ratelimit-remaining' ) ) {
35
+ prefix = 'ratelimit-'
36
+ } else if ( getHeader ( input , 'x-ratelimit-remaining' ) ) {
37
+ prefix = 'x-ratelimit-'
38
+ } else {
39
+ // todo: handle vendor-specific headers here - see https://github.com/ietf-wg-httpapi/ratelimit-headers/issues/25
40
+ return ;
22
41
}
23
- if ( input [ 'ratelimit' ] ) {
24
- return parseCombinedRateLimitHeader ( input [ 'ratelimit' ] )
42
+
43
+ const limit = num ( getHeader ( input , `${ prefix } limit` ) )
44
+ const remaining = num ( getHeader ( input , `${ prefix } remaining` ) )
45
+
46
+ let reset : Date | undefined = undefined ;
47
+ const resetRaw = getHeader ( input , `${ prefix } reset` )
48
+ const resetType = options ?. reset ;
49
+ if ( resetType == 'date' ) reset = parseResetDate ( resetRaw ?? '' ) ;
50
+ else if ( resetType == 'unix' ) reset = parseResetUnix ( resetRaw ?? '' ) ;
51
+ else if ( resetType == 'seconds' ) reset = parseResetSeconds ( resetRaw ?? '' ) ;
52
+ else if ( resetType == 'milliseconds' ) reset = parseResetMilliseconds ( resetRaw ?? '' ) ;
53
+ else if ( resetRaw ) reset = parseResetAuto ( resetRaw )
54
+ else {
55
+ // fallback to retry-after
56
+ const retryAfter = getHeader ( input , 'retry-after' ) ;
57
+ if ( retryAfter ) {
58
+ reset = parseResetUnix ( retryAfter )
59
+ }
60
+ }
61
+
62
+ return {
63
+ limit,
64
+ current : limit - remaining ,
65
+ remaining,
66
+ reset
25
67
}
26
- // todo: handle individual headers
27
68
}
28
69
29
70
const reLimit = / l i m i t \s * = \s * ( \d + ) / i
30
71
const reRemaining = / r e m a i n i n g \s * = \s * ( \d + ) / i
31
72
const reReset = / r e s e t \s * = \s * ( \d + ) / i
32
73
export function parseCombinedRateLimitHeader ( input : string ) : RateLimit {
33
- const limit = parseInt ( reLimit . exec ( input ) ?. [ 1 ] ?? '-1' , 10 ) ;
34
- const remaining = parseInt ( reRemaining . exec ( input ) ?. [ 1 ] ?? '-1' , 10 ) ;
35
- const resetSeconds = reReset . exec ( input ) ?. [ 1 ] ?? '-1' ;
74
+ const limit = num ( reLimit . exec ( input ) ?. [ 1 ] ) ;
75
+ const remaining = num ( reRemaining . exec ( input ) ?. [ 1 ] ) ;
76
+ const resetSeconds = num ( reReset . exec ( input ) ?. [ 1 ] ) ;
36
77
const reset = secondsToDate ( resetSeconds ) ;
37
78
return {
38
79
limit,
@@ -42,8 +83,55 @@ export function parseCombinedRateLimitHeader(input: string): RateLimit {
42
83
}
43
84
}
44
85
45
- function secondsToDate ( seconds : string | number ) : Date {
86
+ function secondsToDate ( seconds : number ) : Date {
46
87
const d = new Date ( ) ;
47
- d . setSeconds ( d . getSeconds ( ) + ( + seconds ) ) ;
88
+ d . setSeconds ( d . getSeconds ( ) + seconds ) ;
48
89
return d ;
90
+ }
91
+
92
+ function num ( input : string | number | undefined ) : number {
93
+ if ( typeof input == 'number' ) return input ;
94
+ return parseInt ( input ?? '' , 10 ) ;
95
+ }
96
+
97
+ function getHeader ( headers : HeadersObject , name : string ) : string | undefined {
98
+ if ( 'get' in headers && typeof headers . get === 'function' ) {
99
+ return headers . get ( name ) ?? undefined // returns null if missing, but everything else is undefined for missing values
100
+ }
101
+ return headers [ name ]
102
+ }
103
+
104
+ function parseResetDate ( resetRaw : string ) : Date {
105
+ // todo: take the server's date into account, calculate an offset, then apply that to the current date
106
+ return new Date ( resetRaw ) ;
107
+ }
108
+
109
+ function parseResetUnix ( resetRaw : string | number ) : Date {
110
+ let resetNum = num ( resetRaw ) ;
111
+ return new Date ( resetNum * 1000 ) ;
112
+ }
113
+
114
+ function parseResetSeconds ( resetRaw : string | number ) : Date {
115
+ let resetNum = num ( resetRaw ) ;
116
+ return secondsToDate ( resetNum ) ;
117
+ }
118
+
119
+ function parseResetMilliseconds ( resetRaw : string | number ) : Date {
120
+ let resetNum = num ( resetRaw ) ;
121
+ return secondsToDate ( resetNum / 1000 ) ;
122
+ }
123
+
124
+ const reLetters = / [ a - z ] / i;
125
+ function parseResetAuto ( resetRaw : string ) : Date {
126
+ // if it has any letters, assume it's a date string
127
+ if ( resetRaw . match ( reLetters ) ) {
128
+ return parseResetDate ( resetRaw )
129
+ }
130
+ const resetNum = num ( resetRaw ) ;
131
+ // looks like a unix timestamp
132
+ if ( resetNum && resetNum > 1000000000 ) { // sometime in 2001
133
+ return parseResetUnix ( resetNum )
134
+ }
135
+ // could be seconds or milliseconds (or something else!), defaulting to seconds
136
+ return parseResetSeconds ( resetNum ) ;
49
137
}
0 commit comments