Skip to content

Commit cef0995

Browse files
committed
Auth token enhancements, try to log ratelimit status
1 parent 219431b commit cef0995

File tree

4 files changed

+141
-18
lines changed

4 files changed

+141
-18
lines changed

app/dashboards.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ async function dashboardsHandler (req, res, next) {
137137
}
138138

139139
if (!commit) {
140-
res.sendStatus(404)
140+
res.status(200).send('No matching commit or branch found')
141141
return
142142
}
143143

@@ -164,7 +164,7 @@ async function dashboardsHandler (req, res, next) {
164164
return handleQueueError(error)
165165
})
166166
} else {
167-
return res.sendStatus(404)
167+
return res.status(200).send('Unable to access cached dashboard files')
168168
}
169169
}
170170

app/download.js

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const { writeFile } = require('./filesystem.js')
1111
const { log } = require('./utilities.js')
1212
const { get: httpsGet } = require('https')
1313
const { join } = require('path')
14-
const authToken = { Authorization: `token ${token}` }
14+
const authToken = token ? { Authorization: `token ${token}` } : {}
1515

1616
const degit = require('tiged')
1717

@@ -22,6 +22,46 @@ const branchInfoCache = new Map()
2222
const commitInfoCache = new Map()
2323
let githubRequest
2424

25+
/**
26+
* Extracts rate limit information from a headers object.
27+
* @param {import('http').IncomingHttpHeaders|undefined} headers
28+
*/
29+
function getRateLimitInfo (headers) {
30+
if (!headers) {
31+
return { remaining: undefined, reset: undefined }
32+
}
33+
34+
const remainingHeader = headers['x-ratelimit-remaining'] ?? headers['X-RateLimit-Remaining']
35+
const resetHeader = headers['x-ratelimit-reset'] ?? headers['X-RateLimit-Reset']
36+
37+
const remaining = Number.parseInt(remainingHeader, 10)
38+
const reset = Number.parseInt(resetHeader, 10)
39+
40+
return {
41+
remaining: Number.isNaN(remaining) ? undefined : remaining,
42+
reset: Number.isNaN(reset) ? undefined : reset
43+
}
44+
}
45+
46+
/**
47+
* Logs a warning when rate limit remaining is depleted.
48+
* @param {import('http').IncomingHttpHeaders|undefined} headers
49+
* @param {string} context
50+
* @returns {{ remaining: number|undefined, reset: number|undefined }}
51+
*/
52+
function logRateLimitIfDepleted (headers, context) {
53+
const { remaining, reset } = getRateLimitInfo(headers)
54+
55+
if (remaining === 0) {
56+
const resetTime = reset
57+
? new Date(reset * 1000).toISOString()
58+
: 'unknown'
59+
log(2, `GitHub API rate limit exhausted while ${context}. Next reset: ${resetTime}`)
60+
}
61+
62+
return { remaining, reset }
63+
}
64+
2565
/**
2666
* Resolve a value from cache or execute the provided fetcher.
2767
* Shares in-flight fetches and caches both positive and negative responses.
@@ -69,7 +109,7 @@ function getWithCache (cache, key, fetcher) {
69109
* @param {string} outputPath The path to output the content at the URL.
70110
*/
71111
async function downloadFile (url, outputPath) {
72-
const { body, statusCode } = await get(url)
112+
const { body, statusCode, headers } = await get(url)
73113
const result = {
74114
outputPath,
75115
statusCode,
@@ -82,6 +122,13 @@ async function downloadFile (url, outputPath) {
82122
await writeFile(outputPath, body)
83123
result.success = true
84124
}
125+
const { remaining, reset } = logRateLimitIfDepleted(headers, `downloading ${url}`)
126+
if (typeof remaining === 'number') {
127+
result.rateLimitRemaining = remaining
128+
}
129+
if (typeof reset === 'number') {
130+
result.rateLimitReset = reset
131+
}
85132

86133
return result
87134
}
@@ -217,7 +264,11 @@ function get(options) {
217264
response.setEncoding('utf8')
218265
response.on('data', (data) => { body.push(data) })
219266
response.on('end', () =>
220-
resolve({ statusCode: response.statusCode, body: body.join('') })
267+
resolve({
268+
statusCode: response.statusCode,
269+
body: body.join(''),
270+
headers: response.headers
271+
})
221272
)
222273
})
223274
request.on('error', reject)

app/handlers.js

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,55 @@ const PATH_TMP_DIRECTORY = join(__dirname, '../tmp')
4444
const URL_DOWNLOAD = `https://raw.githubusercontent.com/${repo}/`
4545

4646
const queue = new JobQueue()
47+
const RATE_LIMIT_HEADER_REMAINING = 'X-GitHub-RateLimit-Remaining'
48+
const RATE_LIMIT_HEADER_RESET = 'X-GitHub-RateLimit-Reset'
49+
50+
/**
51+
* Extract rate limit metadata from a source object.
52+
* @param {{ rateLimitRemaining?: number, rateLimitReset?: number }|null|undefined} source
53+
* @returns {{ rateLimitRemaining?: number, rateLimitReset?: number }|null}
54+
*/
55+
function extractRateLimitMeta (source) {
56+
const remaining = (typeof source?.rateLimitRemaining === 'number') ? source.rateLimitRemaining : undefined
57+
const reset = (typeof source?.rateLimitReset === 'number') ? source.rateLimitReset : undefined
58+
59+
if (remaining === undefined && reset === undefined) {
60+
return null
61+
}
62+
63+
return { rateLimitRemaining: remaining, rateLimitReset: reset }
64+
}
65+
66+
/**
67+
* Attach rate limit metadata to a result object without mutating the original.
68+
* @template T
69+
* @param {T} target
70+
* @param {{ rateLimitRemaining?: number, rateLimitReset?: number }|null} meta
71+
* @returns {T}
72+
*/
73+
function applyRateLimitMeta (target, meta) {
74+
if (!target || !meta) {
75+
return target
76+
}
77+
78+
const needsRemaining = meta.rateLimitRemaining !== undefined && target.rateLimitRemaining === undefined
79+
const needsReset = meta.rateLimitReset !== undefined && target.rateLimitReset === undefined
80+
81+
if (!needsRemaining && !needsReset) {
82+
return target
83+
}
84+
85+
const result = { ...target }
86+
87+
if (needsRemaining) {
88+
result.rateLimitRemaining = meta.rateLimitRemaining
89+
}
90+
if (needsReset) {
91+
result.rateLimitReset = meta.rateLimitReset
92+
}
93+
94+
return result
95+
}
4796

4897
/**
4998
* Tries to look for a remote tsconfig file if the branch/ref is of newer date typically 2019+.
@@ -111,19 +160,21 @@ async function handlerDefault (req, res) {
111160
// Serve a file depending on the request URL.
112161
// Try to serve a static file.
113162
let result = await serveStaticFile(branch, url)
163+
const rateLimitMeta = extractRateLimitMeta(result)
114164

115165
if (result?.status !== 200) {
116166
// Try to build the file
117-
result = await serveBuildFile(branch, url, useGitDownloader)
167+
const buildResult = await serveBuildFile(branch, url, useGitDownloader)
168+
result = applyRateLimitMeta(buildResult, rateLimitMeta)
118169
}
119170

120171
// await updateBranchAccess(join(PATH_TMP_DIRECTORY, branch))
121172

122173
if (!result) {
123-
result = {
124-
status: 404,
174+
result = applyRateLimitMeta({
175+
status: 200,
125176
body: 'Not Found'
126-
}
177+
}, rateLimitMeta)
127178
}
128179

129180
if (!res.headersSent) {
@@ -221,6 +272,8 @@ async function handlerUpdate (req, res) {
221272
* @param {import('express').Request} request Express request object.
222273
*/
223274
async function respondToClient (result, response, request) {
275+
const rateLimitRemaining = (typeof result?.rateLimitRemaining === 'number') ? result.rateLimitRemaining : undefined
276+
const rateLimitReset = (typeof result?.rateLimitReset === 'number') ? result.rateLimitReset : undefined
224277
const { body, file, status } = result
225278
// Make sure connection is not lost before attemting a response.
226279
if (request.connectionAborted || response.headersSent) {
@@ -239,6 +292,13 @@ async function respondToClient (result, response, request) {
239292
response.header('Cache-Control', 'max-age=3600')
240293
response.header('CDN-Cache-Control', 'max-age=3600')
241294

295+
if (rateLimitRemaining !== undefined) {
296+
response.header(RATE_LIMIT_HEADER_REMAINING, String(rateLimitRemaining))
297+
}
298+
if (rateLimitReset !== undefined) {
299+
response.header(RATE_LIMIT_HEADER_RESET, String(rateLimitReset))
300+
}
301+
242302
if (file) {
243303
await new Promise((resolve, reject) => {
244304
response.sendFile(file, (err) => {
@@ -313,7 +373,7 @@ async function serveBuildFile (branch, requestURL, useGitDownloader = true) {
313373
return maybeResponse
314374
}
315375

316-
return { status: 404, body: 'could not find' }
376+
return { status: 200, body: 'could not find' }
317377
}
318378

319379
const buildProject = isMastersQuery
@@ -378,7 +438,7 @@ ${error.message}`)
378438
).then(() => {
379439
return { status: 200, file: pathOutputFile }
380440
}).catch(() => {
381-
return { status: 404, body: 'Server busy, try again in a few minutes' }
441+
return { status: 200, body: 'Server busy, try again in a few minutes' }
382442
})
383443
}
384444

@@ -476,6 +536,7 @@ ${error.message}`)
476536
*/
477537
async function serveStaticFile (branch, requestURL) {
478538
const file = getFile(branch, 'classic', requestURL)
539+
let rateLimitMeta = null
479540

480541
// Respond with not found if the interpreter can not find a filename.
481542
if (file === false) {
@@ -488,11 +549,15 @@ async function serveStaticFile (branch, requestURL) {
488549
if (!existsSync(fileLocation)) {
489550
const urlFile = `${URL_DOWNLOAD}${branch}/${file}`
490551
const download = await downloadFile(urlFile, fileLocation)
552+
const downloadMeta = extractRateLimitMeta(download)
553+
if (downloadMeta) {
554+
rateLimitMeta = downloadMeta
555+
}
491556
if (download.success) {
492-
return { status: 200, file: fileLocation }
557+
return applyRateLimitMeta({ status: 200, file: fileLocation }, rateLimitMeta)
493558
}
494559
} else {
495-
return { status: 200, file: fileLocation }
560+
return applyRateLimitMeta({ status: 200, file: fileLocation }, rateLimitMeta)
496561
}
497562
}
498563

@@ -502,18 +567,25 @@ async function serveStaticFile (branch, requestURL) {
502567
if (!exists(pathFile)) {
503568
const urlFile = `${URL_DOWNLOAD}${branch}/js/${file}`
504569
const download = await downloadFile(urlFile, pathFile)
570+
const downloadMeta = extractRateLimitMeta(download)
571+
if (downloadMeta) {
572+
rateLimitMeta = downloadMeta
573+
}
505574
if (download.statusCode !== 200) {
506575
// we don't always know if it is a static file before we have tried to download it.
507576
// check if this branch contains TypeScript config (we then need to compile it).
508577
if (file.split('/').length <= 1 || await shouldDownloadTypeScriptFolders(URL_DOWNLOAD, branch)) {
509-
return serveBuildFile(branch, requestURL)
578+
const buildResult = await serveBuildFile(branch, requestURL)
579+
return applyRateLimitMeta(buildResult, rateLimitMeta)
510580
}
511-
return response.missingFile
581+
return applyRateLimitMeta({ ...response.missingFile }, rateLimitMeta)
512582
}
583+
584+
return applyRateLimitMeta({ status: 200, file: pathFile }, rateLimitMeta)
513585
}
514586

515587
// Return path to file location in the cache.
516-
return { status: 200, file: pathFile }
588+
return applyRateLimitMeta({ status: 200, file: pathFile }, rateLimitMeta)
517589
}
518590

519591
function printTreeChildren (children, level = 1, carry) {
@@ -545,7 +617,7 @@ async function handlerFS (req, res) {
545617
return respondToClient({ status: 200, body: `<pre>${textTree}</pre>` }, res, req)
546618
}
547619
}
548-
return respondToClient({ status: 404, body: 'no output folder found for this commit' }, res, req)
620+
return respondToClient({ status: 200, body: 'no output folder found for this commit' }, res, req)
549621
}
550622

551623
return respondToClient(response.missingFile, res, req)

app/message.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"body": "Could not assemble this file. It is likely an error located in the source files."
2626
},
2727
"notFound": {
28-
"status": 404,
28+
"status": 200,
2929
"body": "Not found"
3030
},
3131
"noCache": {

0 commit comments

Comments
 (0)