Cloudflare Worker TypeError: Одноразовое тело - PullRequest
0 голосов
/ 30 апреля 2019

Я пытаюсь использовать Cloudflare Worker для передачи запроса POST на другой сервер.

Выдает исключение JS - оборачиваясь в блог try / catch I 'Я установил, что ошибка:

TypeError: A request with a one-time-use body (it was initialized from a stream, not a buffer) encountered a redirect requiring the body to be retransmitted. To avoid this error in the future, construct this request from a buffer-like body initializer.

Я бы подумал, что это можно решить, просто скопировав Ответ, чтобы он не использовался, например:

return new Response(response.body, { headers: response.headers })

Это не работает.Чего мне не хватает в потоковой передаче против буферизации здесь?

addEventListener('fetch', event => {

  var url = new URL(event.request.url);

  if (url.pathname.startsWith('/blog') || url.pathname === '/blog') {
    if (reqType === 'POST') {
      event.respondWith(handleBlogPost(event, url));
    } else {
      handleBlog(event, url);
    }
  } else {
   event.respondWith(fetch(event.request));
  }
})

async function handleBlog(event, url) {
  var newBlog = "https://foo.com";
  var originUrl = url.toString().replace(
    'https://www.bar.com/blog', newBlog);
  event.respondWith(fetch(originUrl)); 
}

async function handleBlogPost(event, url) {
  try {
    var newBlog = "https://foo.com";
    var srcUrl = "https://www.bar.com/blog";

    const init = {
      method: 'POST',
      headers: event.request.headers,
      body: event.request.body
    };
    var originUrl = url.toString().replace( srcUrl, newBlog );

    const response = await fetch(originUrl, init)

    return new Response(response.body, { headers: response.headers })

  } catch (err) {
    // Display the error stack.
    return new Response(err.stack || err)
  }
}

1 Ответ

3 голосов
/ 02 мая 2019

Несколько проблем здесь.

Во-первых, сообщение об ошибке касается тела запроса, а не тела ответа.

По умолчанию объекты Request и Response получены отсеть имеет потоковые тела - request.body и response.body оба имеют тип ReadableStream.Когда вы пересылаете их, тело передает поток - чанки получаются от отправителя и пересылаются конечному получателю, не сохраняя копию локально.Поскольку копии не сохраняются, поток может быть отправлен только один раз.

Однако проблема в вашем случае заключается в том, что после потоковой передачи тела запроса на сервер источника источник ответил 301, 302, 307.или 308 редирект.Эти перенаправления требуют, чтобы клиент повторно передал точно такой же запрос на новый URL-адрес (в отличие от перенаправления 303, которое предписывает клиенту отправить запрос GET на новый URL-адрес).Но Cloudflare Workers не сохранили копию тела запроса, поэтому не могут отправить его снова!

Вы заметите, что эта проблема не возникает, когда вы делаете fetch(event.request), даже еслиЗапрос это ПОЧТА.Причина в том, что для свойства event.request redirect установлено значение "manual", что означает, что fetch() не будет пытаться автоматически выполнять перенаправления.Вместо этого fetch() в этом случае возвращает сам ответ 3xx о перенаправлении и позволяет приложению справиться с ним.Если вы вернете этот ответ в браузер клиента, браузер позаботится о том, чтобы фактически выполнить перенаправление.

Однако в вашем работнике, похоже, fetch() пытается автоматически выполнить перенаправление и создаетошибка.Причина в том, что вы не установили свойство redirect при создании объекта Request:

const init = {
  method: 'POST',
  headers: event.request.headers,
  body: event.request.body
};
// ...
await fetch(originUrl, init)

Поскольку init.redirect не было установлено, fetch() использует поведение по умолчанию, котороесовпадает с redirect = "automatic", т.е. fetch() пытается следовать перенаправлениям.Если вы хотите, чтобы fetch() использовал поведение перенаправления вручную, вы можете добавить redirect: "manual" к init.Тем не менее, похоже, что вы действительно пытаетесь здесь скопировать весь запрос.В этом случае вы должны просто передать event.request вместо структуры init:

// Copy all properties from event.request *except* URL.
await fetch(originUrl, event.request);

Это работает, потому что Request имеет все поля, которые хочет второй параметр fetch().

Что делать, если вам нужны автоматические перенаправления?

Если вы действительно хотите, чтобы fetch() автоматически выполнял перенаправление, тогда вам нужно убедиться, что тело запроса буферизуется, а не передается в потоке, чтобыэто может быть отправлено дважды.Для этого вам нужно будет прочитать все тело в строку или ArrayBuffer, а затем использовать это, например:

const init = {
  method: 'POST',
  headers: event.request.headers,
  // Buffer whole body so that it can be redirected later.
  body: await event.request.arrayBuffer()
};
// ...
await fetch(originUrl, init)

Примечание к ответам

Я быЯ думал, что это можно решить, просто скопировав Ответ так, чтобы он не использовался, например:

return new Response(response.body, { headers: response.headers })

Как описано выше, ошибка, которую вы видите, не связана с этим кодом, но яВ любом случае мне хотелось бы прокомментировать две проблемы.

Во-первых, эта строка кода не копирует все свойства ответа.Например, вам не хватает status и statusText.Есть также некоторые более неясные свойства, которые появляются в определенных ситуациях (например, webSocket, специфичное для Cloudflare расширение спецификации).

Вместо того, чтобы пытаться перечислить каждое свойство, я снова рекомендую просто передатьстарый Response сам объект как структура параметров:

new Response(response.body, response)

Вторая проблема связана с вашим комментарием о копировании.Этот код копирует метаданные Response, но не копирует тело.Это потому, что response.body является ReadableStream.Этот код инициализирует новый объект Respnose, чтобы содержать ссылку на тот же ReadableStream.Когда что-то читает из этого потока, поток используется для обоих Response объектов.

Обычно это нормально, потому что обычно вам нужна только одна копия ответа.Обычно вы просто отправляете это клиенту.Однако есть несколько необычных случаев, когда вы можете отправить ответ в два разных места.Одним из примеров является использование Cache API для кэширования копии ответа.Вы можете сделать это, прочитав все Response в память, как мы делали с запросами выше.Однако для ответов нетривиального размера это может привести к потере памяти и увеличению задержки (вам придется подождать весь ответ, прежде чем любой из них будет отправлен клиенту).

Вместо этого то, что вы действительно хотитев этих необычных случаях нужно выполнить «тройник» потока, чтобы каждый чанк, поступающий из сети, фактически записывался на два разных выхода (например, команда Unix tee, основанная на идее T-перехода в трубе).).

// ONLY use this when there are TWO destinations for the
// response body!
new Response(response.body.tee(), response)

Или, как ярлык (когда вам не нужно изменять какие-либо заголовки), вы можете написать:

// ONLY use this when there are TWO destinations for the
// response body!
response.clone()

Смущенно, response.clone() что-то делает полностью отличается от new Response(response.body, response).response.clone() указывает на тело ответа, но сохраняет заголовки неизменяемыми (если они были неизменными на оригинале).new Response(response.body, response) разделяет ссылку на тот же основной поток, но клонирует заголовки и делает их изменяемыми.Я лично нахожу это довольно странным, но это то, что определяет стандарт Fetch API.

...