diff --git a/lib/fetch.js b/lib/fetch.js index 3ae061c32..8aadf114d 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -38,15 +38,29 @@ const fetchH1 = h1NoCache({ rejectUnauthorized: false // By default skip auth check for all. }).fetch; +export function skipLoggingFetchError(error) { + return error?.name === 'AbortError' + || error?.code === 'ECONNRESET' + || error?.code === 'ERR_STREAM_PREMATURE_CLOSE' + || error?.code === 'ERR_HTTP2_STREAM_ERROR' + || error?.code === 'ERR_HTTP2_SESSION_ERROR'; +} + function doFetch(fetch_func, h1_fetch_func, options) { const fetch_options = Object.assign({}, options); + const is_head_request = fetch_options.method === 'HEAD'; // Implement `qs` (get params). var uri = options.qs ? createUrl(options.uri, options.qs) : options.uri; // Remove hash part of url. uri = uri.replace(/#.*/gi, ''); + // Prevent decode body for head request. + if (is_head_request) { + fetch_options.decode = false; + } + const abortController = new HostAbortController(uri); // Allow request abort before finish. @@ -73,7 +87,7 @@ function doFetch(fetch_func, h1_fetch_func, options) { // Catch async stream errors (e.g. brotli/gzip decode on truncated responses) to avoid unhandled 'error' crashes. stream.on('error', (err) => { clearTimeout(timeoutTimerId); - if (err?.name === 'AbortError') return; + if (skipLoggingFetchError(err)) return; log(' -- doFetch stream error', uri, err?.code, err?.name, err?.message); }); abortController.onResponse(stream); @@ -91,6 +105,10 @@ function doFetch(fetch_func, h1_fetch_func, options) { stream.headers = headers; stream.abortController = abortController; stream.h2 = response.httpVersion === '2.0'; + // HEAD body is empty and no caller reads it; drain so 'end' fires and the socket is released. + if (is_head_request) { + stream.resume(); + } resolve(stream); } }) @@ -143,9 +161,17 @@ export function fetchData(options) { var json = options.json; delete options.json; var res; + const fetch_options = Object.assign({}, options); + const is_head_request = fetch_options.method === 'HEAD'; + const uri = options.qs ? createUrl(options.uri, options.qs) : options.uri; + // Prevent decode body for head request. + if (is_head_request) { + fetch_options.decode = false; + } + const abortController = new HostAbortController(uri); // Allow request abort before finish. @@ -160,26 +186,34 @@ export function fetchData(options) { a_fetch_func(uri, fetch_options) .then(response => { var stream = response.body; - // TODO: looks like HEAD request has no END event. stream.on('end', () => { clearTimeout(timeoutTimerId); }); // Catch async stream errors (e.g. brotli/gzip decode on truncated responses) to avoid unhandled 'error' crashes. stream.on('error', (err) => { clearTimeout(timeoutTimerId); - if (err?.name === 'AbortError') return; + if (skipLoggingFetchError(err)) return; log(' -- fetchData stream error', uri, err?.code, err?.name, err?.message); }); abortController.onResponse(stream); res = response; - if (json !== false) { - // If `json` not forbidden, read `content-type`. - json = json || (response.headers.get('content-type').indexOf('application/json') > -1); - } - if (json) { - return response.json(); + + if (is_head_request) { + // Empty data for HEAD request — drain body so 'end' fires and the socket is released. + stream.resume(); + return Promise.resolve(''); } else { - return response.text(); + + if (json !== false) { + // If `json` not forbidden, read `content-type`. + json = json || (response.headers.get('content-type').indexOf('application/json') > -1); + } + + if (json) { + return response.json(); + } else { + return response.text(); + } } }) .then(data => { diff --git a/lib/plugins/system/htmlparser/htmlparser.js b/lib/plugins/system/htmlparser/htmlparser.js index 00e765ca7..ffee38009 100644 --- a/lib/plugins/system/htmlparser/htmlparser.js +++ b/lib/plugins/system/htmlparser/htmlparser.js @@ -4,7 +4,7 @@ import { cache } from '../../../cache.js'; import * as utils from '../../../utils.js'; import * as libUtils from '../../../utils.js'; import * as metaUtils from '../meta/utils.js'; -import { extendCookiesJar } from '../../../fetch.js'; +import { extendCookiesJar, skipLoggingFetchError } from '../../../fetch.js'; var getUrlFunctional = utils.getUrlFunctional; import { CollectingHandlerForMutliTarget } from './CollectingHandlerForMutliTarget.js'; import { HtmlHandler } from './HtmlHandler.js'; @@ -225,7 +225,7 @@ export default { resp.on('end', parser.end.bind(parser)); // Handle stream error. resp.on('error', function(err) { - if (err?.name !== 'AbortError' && options.handleRuntimeError) { + if (!skipLoggingFetchError(err) && options.handleRuntimeError) { options.handleRuntimeError({ url: url, code: err?.code, diff --git a/lib/request.js b/lib/request.js index 536c6c598..2a13ee4fd 100644 --- a/lib/request.js +++ b/lib/request.js @@ -62,6 +62,7 @@ export default function(options, iframely_options, callback) { } delete options.prepareResult; + delete options.allowCache; delete options.useCacheOnly; delete options.cache_key; delete options.new_cache_key;