const { promisify } = require('util') const { promisifyMultiResult } = require('@overleaf/promise-utils') const Settings = require('@overleaf/settings') const Errors = require('./Errors') const Metrics = require('./Metrics') const logger = require('@overleaf/logger') const request = require('requestretry').defaults({ maxAttempts: 2, retryDelay: 10, }) // We have to be quick with HTTP calls because we're holding a lock that // expires after 30 seconds. We can't let any errors in the rest of the stack // hold us up, and need to bail out quickly if there is a problem. const MAX_HTTP_REQUEST_LENGTH = 5000 // 5 seconds function updateMetric(method, error, response) { // find the status, with special handling for connection timeouts // https://github.com/request/request#timeouts let status if (error && error.connect === true) { status = `${error.code} (connect)` } else if (error) { status = error.code } else if (response) { status = response.statusCode } Metrics.inc(method, 1, { status }) if (error && error.attempts > 1) { Metrics.inc(`${method}-retries`, 1, { status: 'error' }) } if (response && response.attempts > 1) { Metrics.inc(`${method}-retries`, 1, { status: 'success' }) } } function getDoc(projectId, docId, options = {}, _callback) { const timer = new Metrics.Timer('persistenceManager.getDoc') if (typeof options === 'function') { _callback = options options = {} } const callback = function (...args) { timer.done() _callback(...args) } const urlPath = `/project/${projectId}/doc/${docId}` const requestParams = { url: `${Settings.apis.web.url}${urlPath}`, method: 'GET', headers: { accept: 'application/json', }, auth: { user: Settings.apis.web.user, pass: Settings.apis.web.pass, sendImmediately: true, }, jar: false, timeout: MAX_HTTP_REQUEST_LENGTH, } if (options.peek) { requestParams.qs = { peek: 'true' } } request(requestParams, (error, res, body) => { updateMetric('getDoc', error, res) if (error) { logger.error({ err: error, projectId, docId }, 'web API request failed') return callback(new Error('error connecting to web API')) } if (res.statusCode >= 200 && res.statusCode < 300) { try { body = JSON.parse(body) } catch (e) { return callback(e) } if (body.lines == null) { return callback(new Error('web API response had no doc lines')) } if (body.version == null) { return callback(new Error('web API response had no valid doc version')) } if (body.pathname == null) { return callback(new Error('web API response had no valid doc pathname')) } if (!body.pathname) { logger.warn( { projectId, docId }, 'missing pathname in PersistenceManager getDoc' ) Metrics.inc('pathname', 1, { path: 'PersistenceManager.getDoc', status: body.pathname === '' ? 'zero-length' : 'undefined', }) } callback( null, body.lines, body.version, body.ranges, body.pathname, body.projectHistoryId?.toString(), body.historyRangesSupport || false, body.resolvedCommentIds || [] ) } else if (res.statusCode === 404) { callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`)) } else if (res.statusCode === 413) { callback( new Errors.FileTooLargeError(`doc exceeds maximum size: ${urlPath}`) ) } else { callback( new Error(`error accessing web API: ${urlPath} ${res.statusCode}`) ) } }) } function setDoc( projectId, docId, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, _callback ) { const timer = new Metrics.Timer('persistenceManager.setDoc') const callback = function (...args) { timer.done() _callback(...args) } const urlPath = `/project/${projectId}/doc/${docId}` request( { url: `${Settings.apis.web.url}${urlPath}`, method: 'POST', json: { lines, ranges, version, lastUpdatedBy, lastUpdatedAt, }, auth: { user: Settings.apis.web.user, pass: Settings.apis.web.pass, sendImmediately: true, }, jar: false, timeout: MAX_HTTP_REQUEST_LENGTH, }, (error, res, body) => { updateMetric('setDoc', error, res) if (error) { logger.error({ err: error, projectId, docId }, 'web API request failed') return callback(new Error('error connecting to web API')) } if (res.statusCode >= 200 && res.statusCode < 300) { callback(null, body) } else if (res.statusCode === 404) { callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`)) } else if (res.statusCode === 413) { callback( new Errors.FileTooLargeError(`doc exceeds maximum size: ${urlPath}`) ) } else { callback( new Error(`error accessing web API: ${urlPath} ${res.statusCode}`) ) } } ) } module.exports = { getDoc, setDoc, promises: { getDoc: promisifyMultiResult(getDoc, [ 'lines', 'version', 'ranges', 'pathname', 'projectHistoryId', 'historyRangesSupport', 'resolvedCommentIds', ]), setDoc: promisify(setDoc), }, }