fmt(js): npm run fix on merge (#93563)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
34feb375ac
commit
aff18ae21e
@@ -121,7 +121,9 @@ describe('withRetry', () => {
|
||||
() => {
|
||||
attempts += 1
|
||||
|
||||
if (attempts < 3) {return Promise.reject(errWithCode('ECONNRESET'))}
|
||||
if (attempts < 3) {
|
||||
return Promise.reject(errWithCode('ECONNRESET'))
|
||||
}
|
||||
|
||||
return Promise.resolve('ok')
|
||||
},
|
||||
@@ -141,7 +143,9 @@ describe('withRetry', () => {
|
||||
state.bodySent = true
|
||||
attempts += 1
|
||||
|
||||
if (attempts < 2) {return Promise.reject(errWithCode('ECONNREFUSED'))}
|
||||
if (attempts < 2) {
|
||||
return Promise.reject(errWithCode('ECONNREFUSED'))
|
||||
}
|
||||
|
||||
return Promise.resolve(null)
|
||||
},
|
||||
|
||||
@@ -62,7 +62,16 @@ function destroyKeepaliveAgents() {
|
||||
}
|
||||
|
||||
// Transient transport errors: retry MAY be safe (subject to verb gating).
|
||||
const TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN', 'ENOTFOUND', 'EHOSTUNREACH', 'ENETUNREACH'])
|
||||
const TRANSIENT_CODES = new Set([
|
||||
'ECONNRESET',
|
||||
'ECONNREFUSED',
|
||||
'EPIPE',
|
||||
'ETIMEDOUT',
|
||||
'EAI_AGAIN',
|
||||
'ENOTFOUND',
|
||||
'EHOSTUNREACH',
|
||||
'ENETUNREACH'
|
||||
])
|
||||
|
||||
// Errors that prove the request never reached the server: the TCP connection
|
||||
// (or name resolution) failed outright, so nothing was submitted.
|
||||
@@ -75,9 +84,13 @@ function isIdempotentMethod(method) {
|
||||
}
|
||||
|
||||
function isTransientTransportError(error) {
|
||||
if (!error) {return false}
|
||||
if (!error) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (TRANSIENT_CODES.has(error.code)) {return true}
|
||||
if (TRANSIENT_CODES.has(error.code)) {
|
||||
return true
|
||||
}
|
||||
const msg = String(error.message || '')
|
||||
|
||||
return msg.includes('socket hang up') || msg.includes('read ECONNRESET')
|
||||
@@ -93,14 +106,22 @@ function isTransientTransportError(error) {
|
||||
* flushed, so a `false` here proves nothing went out.
|
||||
*/
|
||||
function shouldRetryRequest(error, method, requestState: any = {}) {
|
||||
if (!isTransientTransportError(error)) {return false}
|
||||
if (!isTransientTransportError(error)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isIdempotentMethod(method)) {return true}
|
||||
if (isIdempotentMethod(method)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Non-idempotent: only when the request provably never reached the server.
|
||||
if (NEVER_SENT_CODES.has(error && error.code)) {return true}
|
||||
if (NEVER_SENT_CODES.has(error && error.code)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (requestState.bodySent === false) {return true}
|
||||
if (requestState.bodySent === false) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Ambiguous (reset/hang-up after the body was flushed): the server may have
|
||||
// processed it. Surface the error rather than risk a double submit.
|
||||
@@ -118,7 +139,8 @@ function shouldRetryRequest(error, method, requestState: any = {}) {
|
||||
async function withRetry(makeAttempt, options: any = {}) {
|
||||
const method = String(options.method || 'GET').toUpperCase()
|
||||
const maxRetries = Number.isInteger(options.maxRetries) ? options.maxRetries : 2
|
||||
const delayFn = options.delayFn || (attempt => new Promise(r => setTimeout(r, Math.min(200 * Math.pow(2, attempt), 2000))))
|
||||
const delayFn =
|
||||
options.delayFn || (attempt => new Promise(r => setTimeout(r, Math.min(200 * Math.pow(2, attempt), 2000))))
|
||||
|
||||
let lastError
|
||||
|
||||
|
||||
@@ -54,4 +54,3 @@ test('finalizeGatewayDownload prompts a save dialog then streams the response',
|
||||
// HTTP errors carry their status so a 404 can trigger the fallback.
|
||||
assert.match(fn, /error\.statusCode = statusCode/)
|
||||
})
|
||||
|
||||
|
||||
+173
-165
@@ -4864,106 +4864,110 @@ function fetchJson(url, token, options: any = {}) {
|
||||
// Retry policy lives in api-transport.ts: idempotent verbs retry on any
|
||||
// transient transport error; POST/PUT/DELETE only when the request provably
|
||||
// never reached the server (see shouldRetryRequest) — never double-submit.
|
||||
return withRetry((requestState: any) => new Promise((resolve, reject) => {
|
||||
const { body, contentType } = options.upload
|
||||
? multipartBody(options.upload)
|
||||
: {
|
||||
body: options.body === undefined ? undefined : Buffer.from(JSON.stringify(options.body)),
|
||||
contentType: 'application/json'
|
||||
return withRetry(
|
||||
(requestState: any) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const { body, contentType } = options.upload
|
||||
? multipartBody(options.upload)
|
||||
: {
|
||||
body: options.body === undefined ? undefined : Buffer.from(JSON.stringify(options.body)),
|
||||
contentType: 'application/json'
|
||||
}
|
||||
|
||||
const parsed = new URL(url)
|
||||
const client = parsed.protocol === 'https:' ? https : http
|
||||
const agent = jsonAgentFor(parsed.protocol)
|
||||
const timeoutMs = resolveTimeoutMs(options.timeoutMs, DEFAULT_FETCH_TIMEOUT_MS)
|
||||
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
reject(new Error(`Unsupported Hermes backend URL protocol: ${parsed.protocol}`))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const parsed = new URL(url)
|
||||
const client = parsed.protocol === 'https:' ? https : http
|
||||
const agent = jsonAgentFor(parsed.protocol)
|
||||
const timeoutMs = resolveTimeoutMs(options.timeoutMs, DEFAULT_FETCH_TIMEOUT_MS)
|
||||
const req = client.request(
|
||||
parsed,
|
||||
{
|
||||
agent,
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
...headersForRemoteRequest(url),
|
||||
...(options.headers || {}),
|
||||
'Content-Type': contentType,
|
||||
'X-Hermes-Session-Token': token,
|
||||
// RFC 8252 native flow authenticates the gated gateway with a bearer
|
||||
// token instead of the loopback session-token header. When
|
||||
// ``options.bearer`` is set we send Authorization: Bearer <token>;
|
||||
// the gateway's OAuth gate verifies it via the provider stack with
|
||||
// no cookie involved.
|
||||
...(options.bearer ? { Authorization: `Bearer ${options.bearer}` } : {}),
|
||||
...(body ? { 'Content-Length': String(body.length) } : {})
|
||||
}
|
||||
},
|
||||
res => {
|
||||
const chunks = []
|
||||
res.on('error', reject)
|
||||
res.on('data', chunk => chunks.push(chunk))
|
||||
res.on('end', () => {
|
||||
const text = Buffer.concat(chunks).toString('utf8')
|
||||
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
reject(new Error(`Unsupported Hermes backend URL protocol: ${parsed.protocol}`))
|
||||
if ((res.statusCode || 500) >= 400) {
|
||||
reject(new Error(`${res.statusCode}: ${text || res.statusMessage}`))
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const req = client.request(
|
||||
parsed,
|
||||
{
|
||||
agent,
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
...headersForRemoteRequest(url),
|
||||
...(options.headers || {}),
|
||||
'Content-Type': contentType,
|
||||
'X-Hermes-Session-Token': token,
|
||||
// RFC 8252 native flow authenticates the gated gateway with a bearer
|
||||
// token instead of the loopback session-token header. When
|
||||
// ``options.bearer`` is set we send Authorization: Bearer <token>;
|
||||
// the gateway's OAuth gate verifies it via the provider stack with
|
||||
// no cookie involved.
|
||||
...(options.bearer ? { Authorization: `Bearer ${options.bearer}` } : {}),
|
||||
...(body ? { 'Content-Length': String(body.length) } : {})
|
||||
}
|
||||
},
|
||||
res => {
|
||||
const chunks = []
|
||||
res.on('error', reject)
|
||||
res.on('data', chunk => chunks.push(chunk))
|
||||
res.on('end', () => {
|
||||
const text = Buffer.concat(chunks).toString('utf8')
|
||||
if (!text) {
|
||||
resolve(null)
|
||||
|
||||
if ((res.statusCode || 500) >= 400) {
|
||||
reject(new Error(`${res.statusCode}: ${text || res.statusMessage}`))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
// A 2xx response whose body is HTML means the request fell through
|
||||
// to the SPA index.html (e.g. an unregistered /api path). JSON.parse
|
||||
// would throw an opaque `Unexpected token '<'` here, so surface a
|
||||
// clear diagnostic with the offending URL instead.
|
||||
const looksHtml = /^\s*<(?:!doctype|html)/i.test(text)
|
||||
const contentType = String(res.headers['content-type'] || '')
|
||||
|
||||
if (looksHtml || contentType.includes('text/html')) {
|
||||
reject(
|
||||
new Error(
|
||||
`Expected JSON from ${url} but got HTML (status ${res.statusCode}). ` +
|
||||
'The endpoint is likely missing on the Hermes backend.'
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(text))
|
||||
} catch {
|
||||
reject(new Error(`Invalid JSON from ${url} (status ${res.statusCode}): ${text.slice(0, 200)}`))
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (!text) {
|
||||
resolve(null)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// A 2xx response whose body is HTML means the request fell through
|
||||
// to the SPA index.html (e.g. an unregistered /api path). JSON.parse
|
||||
// would throw an opaque `Unexpected token '<'` here, so surface a
|
||||
// clear diagnostic with the offending URL instead.
|
||||
const looksHtml = /^\s*<(?:!doctype|html)/i.test(text)
|
||||
const contentType = String(res.headers['content-type'] || '')
|
||||
|
||||
if (looksHtml || contentType.includes('text/html')) {
|
||||
reject(
|
||||
new Error(
|
||||
`Expected JSON from ${url} but got HTML (status ${res.statusCode}). ` +
|
||||
'The endpoint is likely missing on the Hermes backend.'
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(text))
|
||||
} catch {
|
||||
reject(new Error(`Invalid JSON from ${url} (status ${res.statusCode}): ${text.slice(0, 200)}`))
|
||||
}
|
||||
req.on('error', reject)
|
||||
req.setTimeout(timeoutMs, () => {
|
||||
req.destroy(new Error(`Timed out connecting to Hermes backend after ${timeoutMs}ms`))
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
req.on('error', reject)
|
||||
req.setTimeout(timeoutMs, () => {
|
||||
req.destroy(new Error(`Timed out connecting to Hermes backend after ${timeoutMs}ms`))
|
||||
})
|
||||
// From here the request goes on the wire: a later transport error can no
|
||||
// longer prove the server didn't process it, so non-idempotent verbs must
|
||||
// not be retried past this point.
|
||||
requestState.bodySent = true
|
||||
|
||||
// From here the request goes on the wire: a later transport error can no
|
||||
// longer prove the server didn't process it, so non-idempotent verbs must
|
||||
// not be retried past this point.
|
||||
requestState.bodySent = true
|
||||
if (body) {
|
||||
req.write(body)
|
||||
}
|
||||
|
||||
if (body) {
|
||||
req.write(body)
|
||||
}
|
||||
|
||||
req.end()
|
||||
}), { method: options.method || 'GET' })
|
||||
req.end()
|
||||
}),
|
||||
{ method: options.method || 'GET' }
|
||||
)
|
||||
}
|
||||
|
||||
// Token-auth download that streams the response body straight to a
|
||||
@@ -5031,95 +5035,99 @@ function fetchPublicJson(url, options: any = {}) {
|
||||
// NO ``X-Hermes-Session-Token`` header — used by the auth-mode probe before
|
||||
// any credentials exist, and any time we must not leak a token to an
|
||||
// endpoint that doesn't need one.
|
||||
return withRetry((requestState: any) => new Promise((resolve, reject) => {
|
||||
const body = options.body === undefined ? undefined : Buffer.from(JSON.stringify(options.body))
|
||||
let parsed
|
||||
return withRetry(
|
||||
(requestState: any) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const body = options.body === undefined ? undefined : Buffer.from(JSON.stringify(options.body))
|
||||
let parsed
|
||||
|
||||
try {
|
||||
parsed = new URL(url)
|
||||
} catch (error) {
|
||||
reject(new Error(`Invalid URL: ${error.message}`))
|
||||
try {
|
||||
parsed = new URL(url)
|
||||
} catch (error) {
|
||||
reject(new Error(`Invalid URL: ${error.message}`))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const client = parsed.protocol === 'https:' ? https : http
|
||||
const agent = jsonAgentFor(parsed.protocol)
|
||||
const timeoutMs = resolveTimeoutMs(options.timeoutMs, DEFAULT_FETCH_TIMEOUT_MS)
|
||||
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
reject(new Error(`Unsupported Hermes backend URL protocol: ${parsed.protocol}`))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const req = client.request(
|
||||
parsed,
|
||||
{
|
||||
agent,
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
...headersForRemoteRequest(url),
|
||||
...(options.headers || {}),
|
||||
'Content-Type': 'application/json',
|
||||
...(body ? { 'Content-Length': String(body.length) } : {})
|
||||
return
|
||||
}
|
||||
},
|
||||
res => {
|
||||
const chunks = []
|
||||
res.on('data', chunk => chunks.push(chunk))
|
||||
res.on('end', () => {
|
||||
const text = Buffer.concat(chunks).toString('utf8')
|
||||
|
||||
if ((res.statusCode || 500) >= 400) {
|
||||
reject(new Error(`${res.statusCode}: ${text || res.statusMessage}`))
|
||||
const client = parsed.protocol === 'https:' ? https : http
|
||||
const agent = jsonAgentFor(parsed.protocol)
|
||||
const timeoutMs = resolveTimeoutMs(options.timeoutMs, DEFAULT_FETCH_TIMEOUT_MS)
|
||||
|
||||
return
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
reject(new Error(`Unsupported Hermes backend URL protocol: ${parsed.protocol}`))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const req = client.request(
|
||||
parsed,
|
||||
{
|
||||
agent,
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
...headersForRemoteRequest(url),
|
||||
...(options.headers || {}),
|
||||
'Content-Type': 'application/json',
|
||||
...(body ? { 'Content-Length': String(body.length) } : {})
|
||||
}
|
||||
},
|
||||
res => {
|
||||
const chunks = []
|
||||
res.on('data', chunk => chunks.push(chunk))
|
||||
res.on('end', () => {
|
||||
const text = Buffer.concat(chunks).toString('utf8')
|
||||
|
||||
if ((res.statusCode || 500) >= 400) {
|
||||
reject(new Error(`${res.statusCode}: ${text || res.statusMessage}`))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
resolve(null)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const looksHtml = /^\s*<(?:!doctype|html)/i.test(text)
|
||||
const contentType = String(res.headers['content-type'] || '')
|
||||
|
||||
if (looksHtml || contentType.includes('text/html')) {
|
||||
reject(
|
||||
new Error(
|
||||
`Expected JSON from ${url} but got HTML (status ${res.statusCode}). ` +
|
||||
'The endpoint is likely missing on the Hermes backend.'
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(text))
|
||||
} catch {
|
||||
reject(new Error(`Invalid JSON from ${url} (status ${res.statusCode}): ${text.slice(0, 200)}`))
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (!text) {
|
||||
resolve(null)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const looksHtml = /^\s*<(?:!doctype|html)/i.test(text)
|
||||
const contentType = String(res.headers['content-type'] || '')
|
||||
|
||||
if (looksHtml || contentType.includes('text/html')) {
|
||||
reject(
|
||||
new Error(
|
||||
`Expected JSON from ${url} but got HTML (status ${res.statusCode}). ` +
|
||||
'The endpoint is likely missing on the Hermes backend.'
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(text))
|
||||
} catch {
|
||||
reject(new Error(`Invalid JSON from ${url} (status ${res.statusCode}): ${text.slice(0, 200)}`))
|
||||
}
|
||||
req.on('error', reject)
|
||||
req.setTimeout(timeoutMs, () => {
|
||||
req.destroy(new Error(`Timed out connecting to Hermes backend after ${timeoutMs}ms`))
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
req.on('error', reject)
|
||||
req.setTimeout(timeoutMs, () => {
|
||||
req.destroy(new Error(`Timed out connecting to Hermes backend after ${timeoutMs}ms`))
|
||||
})
|
||||
// Past this point the request is on the wire — see fetchJson.
|
||||
requestState.bodySent = true
|
||||
|
||||
// Past this point the request is on the wire — see fetchJson.
|
||||
requestState.bodySent = true
|
||||
if (body) {
|
||||
req.write(body)
|
||||
}
|
||||
|
||||
if (body) {
|
||||
req.write(body)
|
||||
}
|
||||
|
||||
req.end()
|
||||
}), { method: options.method || 'GET' })
|
||||
req.end()
|
||||
}),
|
||||
{ method: options.method || 'GET' }
|
||||
)
|
||||
}
|
||||
|
||||
function mimeTypeForPath(filePath) {
|
||||
|
||||
Reference in New Issue
Block a user