Merge pull request #32217 from overleaf/ar-log-long-fetches

[fetch-utils] allow long running requests to be logged

GitOrigin-RevId: 19688937b8aefe314b6bcde98c61badf4f250472
This commit is contained in:
Andrew Rumble
2026-03-17 15:48:09 +01:00
committed by Copybot
parent bf1f34bca6
commit 6f8a2ea9e3
3 changed files with 32 additions and 7 deletions

View File

@@ -5,6 +5,12 @@ const fetch = require('node-fetch')
const http = require('node:http')
const https = require('node:https')
let logger
function setLogger(loggerInstance) {
logger = loggerInstance
}
/**
* @import { Response } from 'node-fetch'
*/
@@ -23,7 +29,7 @@ async function fetchJson(url, opts = {}) {
}
async function fetchJsonWithResponse(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts)
const { fetchOpts, detachSignal } = parseOpts(opts, url)
fetchOpts.headers = fetchOpts.headers ?? {}
fetchOpts.headers.Accept = fetchOpts.headers.Accept ?? 'application/json'
@@ -53,7 +59,7 @@ async function fetchStream(url, opts = {}) {
}
async function fetchStreamWithResponse(url, opts = {}) {
const { fetchOpts, abortController, detachSignal } = parseOpts(opts)
const { fetchOpts, abortController, detachSignal } = parseOpts(opts, url)
const response = await performRequest(url, fetchOpts, detachSignal)
if (!response.ok) {
@@ -76,7 +82,7 @@ async function fetchStreamWithResponse(url, opts = {}) {
* @throws {RequestFailedError} if the response has a failure status code
*/
async function fetchNothing(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts)
const { fetchOpts, detachSignal } = parseOpts(opts, url)
const response = await performRequest(url, fetchOpts, detachSignal)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
@@ -108,7 +114,7 @@ async function fetchRedirect(url, opts = {}) {
* @throws {RequestFailedError} if the response has a non redirect status code or missing Location header
*/
async function fetchRedirectWithResponse(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts)
const { fetchOpts, detachSignal } = parseOpts(opts, url)
fetchOpts.redirect = 'manual'
const response = await performRequest(url, fetchOpts, detachSignal)
if (response.status < 300 || response.status >= 400) {
@@ -142,7 +148,7 @@ async function fetchString(url, opts = {}) {
}
async function fetchStringWithResponse(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts)
const { fetchOpts, detachSignal } = parseOpts(opts, url)
const response = await performRequest(url, fetchOpts, detachSignal)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
@@ -167,7 +173,7 @@ class RequestFailedError extends OError {
}
}
function parseOpts(opts) {
function parseOpts(opts, url) {
const fetchOpts = _.omit(opts, ['json', 'signal', 'basicAuth'])
if (opts.json) {
setupJsonBody(fetchOpts, opts.json)
@@ -178,9 +184,21 @@ function parseOpts(opts) {
const abortController = new AbortController()
fetchOpts.signal = abortController.signal
let detachSignal = () => {}
let detachSignal
if (opts.signal) {
detachSignal = abortOnSignal(abortController, opts.signal)
} else {
const timeout = setTimeout(() => {
if (logger) {
logger.warn(
{ url, method: opts.method ?? 'GET' },
'Fetch request did not complete within 120 seconds'
)
}
}, 120000)
detachSignal = () => {
clearTimeout(timeout)
}
}
if (opts.body instanceof Readable) {
abortOnDestroyedRequest(abortController, fetchOpts.body)
@@ -323,4 +341,5 @@ module.exports = {
ConnectTimeoutError,
CustomHttpAgent,
CustomHttpsAgent,
setLogger,
}