CompileController.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. let CompileController
  2. const { URL, URLSearchParams } = require('url')
  3. const { pipeline } = require('stream/promises')
  4. const { Cookie } = require('tough-cookie')
  5. const OError = require('@overleaf/o-error')
  6. const Metrics = require('@overleaf/metrics')
  7. const ProjectGetter = require('../Project/ProjectGetter')
  8. const CompileManager = require('./CompileManager')
  9. const ClsiManager = require('./ClsiManager')
  10. const logger = require('@overleaf/logger')
  11. const Settings = require('@overleaf/settings')
  12. const SessionManager = require('../Authentication/SessionManager')
  13. const { RateLimiter } = require('../../infrastructure/RateLimiter')
  14. const ClsiCookieManager = require('./ClsiCookieManager')(
  15. Settings.apis.clsi?.backendGroupName
  16. )
  17. const Path = require('path')
  18. const AnalyticsManager = require('../Analytics/AnalyticsManager')
  19. const SplitTestHandler = require('../SplitTests/SplitTestHandler')
  20. const { callbackify } = require('@overleaf/promise-utils')
  21. const {
  22. fetchStreamWithResponse,
  23. RequestFailedError,
  24. } = require('@overleaf/fetch-utils')
  25. const COMPILE_TIMEOUT_MS = 10 * 60 * 1000
  26. const pdfDownloadRateLimiter = new RateLimiter('full-pdf-download', {
  27. points: 1000,
  28. duration: 60 * 60,
  29. })
  30. function getOutputFilesArchiveSpecification(projectId, userId, buildId) {
  31. const fileName = 'output.zip'
  32. return {
  33. path: fileName,
  34. url: CompileController._getFileUrl(projectId, userId, buildId, fileName),
  35. type: 'zip',
  36. }
  37. }
  38. function getImageNameForProject(projectId, callback) {
  39. ProjectGetter.getProject(projectId, { imageName: 1 }, (err, project) => {
  40. if (err) return callback(err)
  41. if (!project) return callback(new Error('project not found'))
  42. callback(null, project.imageName)
  43. })
  44. }
  45. async function getPdfCachingMinChunkSize(req, res) {
  46. const { variant } = await SplitTestHandler.promises.getAssignment(
  47. req,
  48. res,
  49. 'pdf-caching-min-chunk-size'
  50. )
  51. if (variant === 'default') return 1_000_000
  52. return parseInt(variant, 10)
  53. }
  54. const getSplitTestOptions = callbackify(async function (req, res) {
  55. // Use the query flags from the editor request for overriding the split test.
  56. let query = {}
  57. try {
  58. const u = new URL(req.headers.referer || req.url, Settings.siteUrl)
  59. query = Object.fromEntries(u.searchParams.entries())
  60. } catch (e) {}
  61. const editorReq = { ...req, query }
  62. const pdfDownloadDomain = Settings.pdfDownloadDomain
  63. if (!req.query.enable_pdf_caching) {
  64. // The frontend does not want to do pdf caching.
  65. return {
  66. pdfDownloadDomain,
  67. enablePdfCaching: false,
  68. }
  69. }
  70. // Double check with the latest split test assignment.
  71. // We may need to turn off the feature on a short notice, without requiring
  72. // all users to reload their editor page to disable the feature.
  73. const { variant } = await SplitTestHandler.promises.getAssignment(
  74. editorReq,
  75. res,
  76. 'pdf-caching-mode'
  77. )
  78. const enablePdfCaching = variant === 'enabled'
  79. if (!enablePdfCaching) {
  80. // Skip the lookup of the chunk size when caching is not enabled.
  81. return {
  82. pdfDownloadDomain,
  83. enablePdfCaching: false,
  84. }
  85. }
  86. const pdfCachingMinChunkSize = await getPdfCachingMinChunkSize(editorReq, res)
  87. return {
  88. pdfDownloadDomain,
  89. enablePdfCaching,
  90. pdfCachingMinChunkSize,
  91. }
  92. })
  93. module.exports = CompileController = {
  94. compile(req, res, next) {
  95. res.setTimeout(COMPILE_TIMEOUT_MS)
  96. const projectId = req.params.Project_id
  97. const isAutoCompile = !!req.query.auto_compile
  98. const fileLineErrors = !!req.query.file_line_errors
  99. const stopOnFirstError = !!req.body.stopOnFirstError
  100. const userId = SessionManager.getLoggedInUserId(req.session)
  101. const options = {
  102. isAutoCompile,
  103. fileLineErrors,
  104. stopOnFirstError,
  105. }
  106. if (req.body.rootDoc_id) {
  107. options.rootDoc_id = req.body.rootDoc_id
  108. } else if (
  109. req.body.settingsOverride &&
  110. req.body.settingsOverride.rootDoc_id
  111. ) {
  112. // Can be removed after deploy
  113. options.rootDoc_id = req.body.settingsOverride.rootDoc_id
  114. }
  115. if (req.body.compiler) {
  116. options.compiler = req.body.compiler
  117. }
  118. if (req.body.draft) {
  119. options.draft = req.body.draft
  120. }
  121. if (['validate', 'error', 'silent'].includes(req.body.check)) {
  122. options.check = req.body.check
  123. }
  124. if (req.body.incrementalCompilesEnabled) {
  125. options.incrementalCompilesEnabled = true
  126. }
  127. getSplitTestOptions(req, res, (err, splitTestOptions) => {
  128. if (err) return next(err)
  129. let { enablePdfCaching, pdfCachingMinChunkSize, pdfDownloadDomain } =
  130. splitTestOptions
  131. options.enablePdfCaching = enablePdfCaching
  132. if (enablePdfCaching) {
  133. options.pdfCachingMinChunkSize = pdfCachingMinChunkSize
  134. }
  135. CompileManager.compile(
  136. projectId,
  137. userId,
  138. options,
  139. (
  140. error,
  141. status,
  142. outputFiles,
  143. clsiServerId,
  144. limits,
  145. validationProblems,
  146. stats,
  147. timings,
  148. outputUrlPrefix,
  149. buildId
  150. ) => {
  151. if (error) {
  152. Metrics.inc('compile-error')
  153. return next(error)
  154. }
  155. Metrics.inc('compile-status', 1, { status })
  156. if (pdfDownloadDomain && outputUrlPrefix) {
  157. pdfDownloadDomain += outputUrlPrefix
  158. }
  159. if (
  160. limits &&
  161. SplitTestHandler.getPercentile(
  162. AnalyticsManager.getIdsFromSession(req.session).analyticsId,
  163. 'compile-result-backend',
  164. 'release'
  165. ) === 1
  166. ) {
  167. // For a compile request to be sent to clsi we need limits.
  168. // If we get here without having the limits object populated, it is
  169. // a reasonable assumption to make that nothing was compiled.
  170. // We need to know the limits in order to make use of the events.
  171. AnalyticsManager.recordEventForSession(
  172. req.session,
  173. 'compile-result-backend',
  174. {
  175. projectId,
  176. ownerAnalyticsId: limits.ownerAnalyticsId,
  177. status,
  178. compileTime: timings?.compileE2E,
  179. timeout: limits.timeout === 60 ? 'short' : 'long',
  180. server: clsiServerId?.includes('-c2d-') ? 'faster' : 'normal',
  181. isAutoCompile,
  182. stopOnFirstError,
  183. }
  184. )
  185. }
  186. const outputFilesArchive = buildId
  187. ? getOutputFilesArchiveSpecification(projectId, userId, buildId)
  188. : null
  189. res.json({
  190. status,
  191. outputFiles,
  192. outputFilesArchive,
  193. compileGroup: limits?.compileGroup,
  194. clsiServerId,
  195. validationProblems,
  196. stats,
  197. timings,
  198. outputUrlPrefix,
  199. pdfDownloadDomain,
  200. pdfCachingMinChunkSize,
  201. })
  202. }
  203. )
  204. })
  205. },
  206. stopCompile(req, res, next) {
  207. const projectId = req.params.Project_id
  208. const userId = SessionManager.getLoggedInUserId(req.session)
  209. CompileManager.stopCompile(projectId, userId, function (error) {
  210. if (error) {
  211. return next(error)
  212. }
  213. res.sendStatus(200)
  214. })
  215. },
  216. // Used for submissions through the public API
  217. compileSubmission(req, res, next) {
  218. res.setTimeout(COMPILE_TIMEOUT_MS)
  219. const submissionId = req.params.submission_id
  220. const options = {}
  221. if (req.body?.rootResourcePath != null) {
  222. options.rootResourcePath = req.body.rootResourcePath
  223. }
  224. if (req.body?.compiler) {
  225. options.compiler = req.body.compiler
  226. }
  227. if (req.body?.draft) {
  228. options.draft = req.body.draft
  229. }
  230. if (['validate', 'error', 'silent'].includes(req.body?.check)) {
  231. options.check = req.body.check
  232. }
  233. options.compileGroup =
  234. req.body?.compileGroup || Settings.defaultFeatures.compileGroup
  235. options.compileBackendClass = Settings.apis.clsi.submissionBackendClass
  236. options.timeout =
  237. req.body?.timeout || Settings.defaultFeatures.compileTimeout
  238. ClsiManager.sendExternalRequest(
  239. submissionId,
  240. req.body,
  241. options,
  242. function (error, status, outputFiles, clsiServerId, validationProblems) {
  243. if (error) {
  244. return next(error)
  245. }
  246. res.json({
  247. status,
  248. outputFiles,
  249. clsiServerId,
  250. validationProblems,
  251. })
  252. }
  253. )
  254. },
  255. _compileAsUser(req, callback) {
  256. // callback with userId if per-user, undefined otherwise
  257. if (!Settings.disablePerUserCompiles) {
  258. const userId = SessionManager.getLoggedInUserId(req.session)
  259. callback(null, userId)
  260. } else {
  261. callback()
  262. }
  263. }, // do a per-project compile, not per-user
  264. _downloadAsUser(req, callback) {
  265. // callback with userId if per-user, undefined otherwise
  266. if (!Settings.disablePerUserCompiles) {
  267. const userId = SessionManager.getLoggedInUserId(req.session)
  268. callback(null, userId)
  269. } else {
  270. callback()
  271. }
  272. }, // do a per-project compile, not per-user
  273. downloadPdf(req, res, next) {
  274. Metrics.inc('pdf-downloads')
  275. const projectId = req.params.Project_id
  276. const rateLimit = function (callback) {
  277. pdfDownloadRateLimiter
  278. .consume(req.ip, 1, { method: 'ip' })
  279. .then(() => {
  280. callback(null, true)
  281. })
  282. .catch(err => {
  283. if (err instanceof Error) {
  284. callback(err)
  285. } else {
  286. callback(null, false)
  287. }
  288. })
  289. }
  290. ProjectGetter.getProject(projectId, { name: 1 }, function (err, project) {
  291. if (err) {
  292. return next(err)
  293. }
  294. res.contentType('application/pdf')
  295. const filename = `${CompileController._getSafeProjectName(project)}.pdf`
  296. if (req.query.popupDownload) {
  297. res.setContentDisposition('attachment', { filename })
  298. } else {
  299. res.setContentDisposition('inline', { filename })
  300. }
  301. rateLimit(function (err, canContinue) {
  302. if (err) {
  303. logger.err({ err }, 'error checking rate limit for pdf download')
  304. res.sendStatus(500)
  305. } else if (!canContinue) {
  306. logger.debug(
  307. { projectId, ip: req.ip },
  308. 'rate limit hit downloading pdf'
  309. )
  310. res.sendStatus(500)
  311. } else {
  312. CompileController._downloadAsUser(req, function (error, userId) {
  313. if (error) {
  314. return next(error)
  315. }
  316. const url = CompileController._getFileUrl(
  317. projectId,
  318. userId,
  319. req.params.build_id,
  320. 'output.pdf'
  321. )
  322. CompileController.proxyToClsi(
  323. projectId,
  324. 'output-file',
  325. url,
  326. {},
  327. req,
  328. res,
  329. next
  330. )
  331. })
  332. }
  333. })
  334. })
  335. },
  336. _getSafeProjectName(project) {
  337. return project.name.replace(/[^\p{L}\p{Nd}]/gu, '_')
  338. },
  339. deleteAuxFiles(req, res, next) {
  340. const projectId = req.params.Project_id
  341. const { clsiserverid } = req.query
  342. CompileController._compileAsUser(req, function (error, userId) {
  343. if (error) {
  344. return next(error)
  345. }
  346. CompileManager.deleteAuxFiles(
  347. projectId,
  348. userId,
  349. clsiserverid,
  350. function (error) {
  351. if (error) {
  352. return next(error)
  353. }
  354. res.sendStatus(200)
  355. }
  356. )
  357. })
  358. },
  359. // this is only used by templates, so is not called with a userId
  360. compileAndDownloadPdf(req, res, next) {
  361. const projectId = req.params.project_id
  362. // pass userId as null, since templates are an "anonymous" compile
  363. CompileManager.compile(projectId, null, {}, (err, _status, outputFiles) => {
  364. if (err) {
  365. logger.err(
  366. { err, projectId },
  367. 'something went wrong compile and downloading pdf'
  368. )
  369. res.sendStatus(500)
  370. return
  371. }
  372. const pdf = outputFiles.find(f => f.path === 'output.pdf')
  373. if (!pdf) {
  374. logger.warn(
  375. { projectId },
  376. 'something went wrong compile and downloading pdf: no pdf'
  377. )
  378. res.sendStatus(500)
  379. return
  380. }
  381. CompileController.proxyToClsi(
  382. projectId,
  383. 'output-file',
  384. pdf.url,
  385. {},
  386. req,
  387. res,
  388. next
  389. )
  390. })
  391. },
  392. getFileFromClsi(req, res, next) {
  393. const projectId = req.params.Project_id
  394. CompileController._downloadAsUser(req, function (error, userId) {
  395. if (error) {
  396. return next(error)
  397. }
  398. const qs = {}
  399. const url = CompileController._getFileUrl(
  400. projectId,
  401. userId,
  402. req.params.build_id,
  403. req.params.file
  404. )
  405. CompileController.proxyToClsi(
  406. projectId,
  407. 'output-file',
  408. url,
  409. qs,
  410. req,
  411. res,
  412. next
  413. )
  414. })
  415. },
  416. getFileFromClsiWithoutUser(req, res, next) {
  417. const submissionId = req.params.submission_id
  418. const url = CompileController._getFileUrl(
  419. submissionId,
  420. null,
  421. req.params.build_id,
  422. req.params.file
  423. )
  424. const limits = {
  425. compileGroup:
  426. req.body?.compileGroup ||
  427. req.query?.compileGroup ||
  428. Settings.defaultFeatures.compileGroup,
  429. compileBackendClass: Settings.apis.clsi.submissionBackendClass,
  430. }
  431. CompileController.proxyToClsiWithLimits(
  432. submissionId,
  433. 'output-file',
  434. url,
  435. {},
  436. limits,
  437. req,
  438. res,
  439. next
  440. )
  441. },
  442. // compute a GET file url for a given project, user (optional), build (optional) and file
  443. _getFileUrl(projectId, userId, buildId, file) {
  444. let url
  445. if (userId != null && buildId != null) {
  446. url = `/project/${projectId}/user/${userId}/build/${buildId}/output/${file}`
  447. } else if (userId != null) {
  448. url = `/project/${projectId}/user/${userId}/output/${file}`
  449. } else if (buildId != null) {
  450. url = `/project/${projectId}/build/${buildId}/output/${file}`
  451. } else {
  452. url = `/project/${projectId}/output/${file}`
  453. }
  454. return url
  455. },
  456. // compute a POST url for a project, user (optional) and action
  457. _getUrl(projectId, userId, action) {
  458. let path = `/project/${projectId}`
  459. if (userId != null) {
  460. path += `/user/${userId}`
  461. }
  462. return `${path}/${action}`
  463. },
  464. proxySyncPdf(req, res, next) {
  465. const projectId = req.params.Project_id
  466. const { page, h, v } = req.query
  467. if (!page?.match(/^\d+$/)) {
  468. return next(new Error('invalid page parameter'))
  469. }
  470. if (!h?.match(/^-?\d+\.\d+$/)) {
  471. return next(new Error('invalid h parameter'))
  472. }
  473. if (!v?.match(/^-?\d+\.\d+$/)) {
  474. return next(new Error('invalid v parameter'))
  475. }
  476. // whether this request is going to a per-user container
  477. CompileController._compileAsUser(req, function (error, userId) {
  478. if (error) {
  479. return next(error)
  480. }
  481. getImageNameForProject(projectId, (error, imageName) => {
  482. if (error) return next(error)
  483. const url = CompileController._getUrl(projectId, userId, 'sync/pdf')
  484. CompileController.proxyToClsi(
  485. projectId,
  486. 'sync-to-pdf',
  487. url,
  488. { page, h, v, imageName },
  489. req,
  490. res,
  491. next
  492. )
  493. })
  494. })
  495. },
  496. proxySyncCode(req, res, next) {
  497. const projectId = req.params.Project_id
  498. const { file, line, column } = req.query
  499. if (file == null) {
  500. return next(new Error('missing file parameter'))
  501. }
  502. // Check that we are dealing with a simple file path (this is not
  503. // strictly needed because synctex uses this parameter as a label
  504. // to look up in the synctex output, and does not open the file
  505. // itself). Since we have valid synctex paths like foo/./bar we
  506. // allow those by replacing /./ with /
  507. const testPath = file.replace('/./', '/')
  508. if (Path.resolve('/', testPath) !== `/${testPath}`) {
  509. return next(new Error('invalid file parameter'))
  510. }
  511. if (!line?.match(/^\d+$/)) {
  512. return next(new Error('invalid line parameter'))
  513. }
  514. if (!column?.match(/^\d+$/)) {
  515. return next(new Error('invalid column parameter'))
  516. }
  517. CompileController._compileAsUser(req, function (error, userId) {
  518. if (error) {
  519. return next(error)
  520. }
  521. getImageNameForProject(projectId, (error, imageName) => {
  522. if (error) return next(error)
  523. const url = CompileController._getUrl(projectId, userId, 'sync/code')
  524. CompileController.proxyToClsi(
  525. projectId,
  526. 'sync-to-code',
  527. url,
  528. { file, line, column, imageName },
  529. req,
  530. res,
  531. next
  532. )
  533. })
  534. })
  535. },
  536. proxyToClsi(projectId, action, url, qs, req, res, next) {
  537. CompileManager.getProjectCompileLimits(projectId, function (error, limits) {
  538. if (error) {
  539. return next(error)
  540. }
  541. CompileController.proxyToClsiWithLimits(
  542. projectId,
  543. action,
  544. url,
  545. qs,
  546. limits,
  547. req,
  548. res,
  549. next
  550. )
  551. })
  552. },
  553. proxyToClsiWithLimits(projectId, action, url, qs, limits, req, res, next) {
  554. _getPersistenceOptions(
  555. req,
  556. projectId,
  557. limits.compileGroup,
  558. limits.compileBackendClass,
  559. (err, persistenceOptions) => {
  560. if (err) {
  561. OError.tag(err, 'error getting cookie jar for clsi request')
  562. return next(err)
  563. }
  564. url = new URL(`${Settings.apis.clsi.url}${url}`)
  565. url.search = new URLSearchParams({
  566. ...persistenceOptions.qs,
  567. ...qs,
  568. }).toString()
  569. const timer = new Metrics.Timer(
  570. 'proxy_to_clsi',
  571. 1,
  572. { path: action },
  573. [0, 100, 1000, 2000, 5000, 10000, 15000, 20000, 30000, 45000, 60000]
  574. )
  575. Metrics.inc('proxy_to_clsi', 1, { path: action, status: 'start' })
  576. fetchStreamWithResponse(url.href, {
  577. method: req.method,
  578. signal: AbortSignal.timeout(60 * 1000),
  579. headers: persistenceOptions.headers,
  580. })
  581. .then(({ stream, response }) => {
  582. if (req.destroyed) {
  583. // The client has disconnected already, avoid trying to write into the broken connection.
  584. Metrics.inc('proxy_to_clsi', 1, {
  585. path: action,
  586. status: 'req-aborted',
  587. })
  588. return
  589. }
  590. Metrics.inc('proxy_to_clsi', 1, {
  591. path: action,
  592. status: response.status,
  593. })
  594. for (const key of ['Content-Length', 'Content-Type']) {
  595. if (response.headers.has(key)) {
  596. res.setHeader(key, response.headers.get(key))
  597. }
  598. }
  599. res.writeHead(response.status)
  600. return pipeline(stream, res)
  601. })
  602. .then(() => {
  603. timer.labels.status = 'success'
  604. timer.done()
  605. })
  606. .catch(err => {
  607. const reqAborted = Boolean(req.destroyed)
  608. const status = reqAborted ? 'req-aborted-late' : 'error'
  609. timer.labels.status = status
  610. const duration = timer.done()
  611. Metrics.inc('proxy_to_clsi', 1, { path: action, status })
  612. const streamingStarted = Boolean(res.headersSent)
  613. if (!streamingStarted) {
  614. if (err instanceof RequestFailedError) {
  615. res.sendStatus(err.response.status)
  616. } else {
  617. res.sendStatus(500)
  618. }
  619. }
  620. if (
  621. streamingStarted &&
  622. reqAborted &&
  623. err.code === 'ERR_STREAM_PREMATURE_CLOSE'
  624. ) {
  625. // Ignore noisy spurious error
  626. return
  627. }
  628. if (
  629. err instanceof RequestFailedError &&
  630. ['sync-to-code', 'sync-to-pdf', 'output-file'].includes(action)
  631. ) {
  632. // Ignore noisy error
  633. // https://github.com/overleaf/internal/issues/15201
  634. return
  635. }
  636. logger.warn(
  637. {
  638. err,
  639. projectId,
  640. url,
  641. action,
  642. reqAborted,
  643. streamingStarted,
  644. duration,
  645. },
  646. 'CLSI proxy error'
  647. )
  648. })
  649. }
  650. )
  651. },
  652. wordCount(req, res, next) {
  653. const projectId = req.params.Project_id
  654. const file = req.query.file || false
  655. const { clsiserverid } = req.query
  656. CompileController._compileAsUser(req, function (error, userId) {
  657. if (error) {
  658. return next(error)
  659. }
  660. CompileManager.wordCount(
  661. projectId,
  662. userId,
  663. file,
  664. clsiserverid,
  665. function (error, body) {
  666. if (error) {
  667. return next(error)
  668. }
  669. res.json(body)
  670. }
  671. )
  672. })
  673. },
  674. }
  675. function _getPersistenceOptions(
  676. req,
  677. projectId,
  678. compileGroup,
  679. compileBackendClass,
  680. callback
  681. ) {
  682. const { clsiserverid } = req.query
  683. const userId = SessionManager.getLoggedInUserId(req)
  684. if (clsiserverid && typeof clsiserverid === 'string') {
  685. callback(null, {
  686. qs: { clsiserverid, compileGroup, compileBackendClass },
  687. headers: {},
  688. })
  689. } else {
  690. ClsiCookieManager.getServerId(
  691. projectId,
  692. userId,
  693. compileGroup,
  694. compileBackendClass,
  695. (err, clsiServerId) => {
  696. if (err) return callback(err)
  697. callback(null, {
  698. qs: { compileGroup, compileBackendClass },
  699. headers: clsiServerId
  700. ? {
  701. Cookie: new Cookie({
  702. key: Settings.clsiCookie.key,
  703. value: clsiServerId,
  704. }).cookieString(),
  705. }
  706. : {},
  707. })
  708. }
  709. )
  710. }
  711. }