В общем, проблема была именно с
Content-Type, который отдавался в
text/plain.
Самый схожий случай с близлежащей проблемой
описан здесь, а популярные методы решения
тут (не для вышеприведённого).
Первое что пришло в голову после обнаружения корня зла - попытка модифицировать ответ на text/html, с которым не возникало подобного, по
этому варианту, параллельно решая дополнительные
задачи. Всё-бы ничего, но только к этому варианту нужен собственный body для корректной работы. Если же передавать
пустой или попытаться получить с помощью
Fetch.getResponseBody (в рамках описанного варианта), то подключение не пройдёт успешно (страница висит в ожидании ответа), а единственная ошибка, которая вероятнее всего прилетит - таймаут. Описанный ниже
метод ещё более костыльный, так как отправляет запрос через request, от которого я изначально уходил.
В конечном итоге посчастливилось столкнуться
с тем, что нужно и вникать в суть проблемы
более глубже. Собственно, рекомендуют использовать
Fetch.getResponseBody более
нестандартно, что может вызвать конфликты с плагинами (
кооперация появилась относительно недавно, но это лишь часть решения, кроме того
до сих пор не интегрировано в популярный puppeteer... ), ибо puppeteer регулирует
Fetch.enable самостоятельно для другой сессии клиента.
Выложу в данном ответе 2 решения.
Первое, используя единичную стратегию, через Chrome Devtools Protocol -> Fetch с последующим отключением:
const URI = "<someUri>";
//...launch puppeteer
const next = async (browser) => {
let realResponse = null;
const page = await browser.newPage()
const client = await page.target().createCDPSession();
await client.send("Fetch.enable", {
patterns: [{ requestStage: "Response" }]
});
const requestPausedHandler = async (event) => {
const { url } = event.request;
if(url == URI) {
const { requestId } = event;
const responseCdp = await client.send("Fetch.getResponseBody", { requestId });
realResponse = responseCdp.base64Encoded
? Buffer.from(responseCdp.body, "base64").toString("utf-8")
: responseCdp.body;
//При правильном регулировании - возможен вызов закомментированного, но здесь принцип once.
//await client.send("Fetch.continueRequest", { requestId });
//Поэтому следует использовать сразу использовать Fetch.disable для созданной сессии, дабы отключиться от фазы Response (отдаёт ответ параллельно).
await client.send("Fetch.disable");
}
};
client.on("Fetch.requestPaused", requestPausedHandler);
page.goto(uri).then(async (response) => {
client.off("Fetch.requestPaused", requestPausedHandler);
console.log(realResponse);
});
}
И второе, которое описал в конце вопроса. Заключается в том, чтобы получать главную страницу сайта и оттуда выполнять XMLHttpRequest с помощью page.evaluate (предварительно сохраняя оригинальные вспомогательные методы воздействия):
const MAIN_URI = "<someUri>";
const NEED_URI = "<someUri>";
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const primordials = ["Promise", "XMLHttpRequest"];
const defaultLengthOfHiddenIdentifier = 50;
const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const genRandomString = length => {
let result = "";
const alphabetLMinusOne = alphabet.length - 1;
for(let i = 0; i < length; ++i) {
result += alphabet[rand(0, alphabetLMinusOne)];
}
return result;
};
//...launch puppeteer
const next = async (browser) => {
const page = await browser.newPage();
//Запрос на страницу должен идти после сохранения primordials
//await page.goto(MAIN_URI);
//...possible waitUntil
let nowLengthOfHI = defaultLengthOfHiddenIdentifier;
for(let i = 0; i < primordials.length; ++i) {
const object = primordials[i];
let possibleIdentifier = genRandomString(nowLengthOfHI);
for(let j = i - 1; j > 0; --j) {
const prevObject = primordials[j];
if(prevObject[1] == possibleIdentifier) {
possibleIdentifier = genRandomString(++nowLengthOfHI);
break;
}
}
primordials[i] = [
object,
possibleIdentifier,
];
}
//Сохранение оригинальных значений для Promise и XMLHttpRequest, чтобы быть уверенными, что сайт не перезаписал их.
await page.evaluateOnNewDocument(primordials => {
const getOriginObject = object => {
if(object.includes(".")) {
const splitted = object.split(".");
let ret = window;
for(let i = 0; i < splitted.length; ++i) {
ret = ret[splitted[i]];
}
return ret;
}
return window[object];
};
for(let i = 0; i < primordials.length; ++i) {
const object = primordials[i][0];
const identifier = primordials[i][1];
Object.defineProperty(window, identifier, { value: getOriginObject(object) });
}
}, primordials);
await page.goto(MAIN_URI);
const answerObject = await page.evaluate(async (url, primordials) => {
const _Promise = window[ primordials.find(obj => obj[0] == "Promise")[1] ];
const _XMLHttpRequest = window[ primordials.find(obj => obj[0] == "XMLHttpRequest")[1] ];
return new _Promise(resolve => {
let wasCalled = false;
const handleEvent = e => {
if(wasCalled) return;
wasCalled = true;
const answer = {
error: false,
type: e.type,
status: xhr.status,
response: xhr.responseText,
};
if(e.type == "error" || e.type == "abort" || e.type == "timeout") {
answer.error = true;
}
if(xhr.status >= 400 && xhr.status < 600) {
answer.error = true;
}
return resolve(answer);
};
const xhr = new _XMLHttpRequest();
xhr.addEventListener('error', handleEvent);
xhr.addEventListener('timeout', handleEvent);
xhr.addEventListener('load', handleEvent);
xhr.open("GET", url);
xhr.send();
});
}, NEED_URI, primordials);
console.log(answerObject);
}