Cloudflare Worker TypeError: One-time-use body

Cloudflare Worker TypeError: One-time-use body

我正在尝试使用 Cloudflare Worker 将 POST 请求代理到另一台服务器。

它抛出一个 JS 异常——通过包装在一个 try/catch 博客中,我确定错误是:

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.

我原以为这可以通过简单地复制 Response 使其不被使用来解决,就像这样:

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)
  }
}

这里有几个问题。

首先,错误消息是关于请求 body,而不是响应 body。

默认情况下,从网络接收到的RequestResponseobjects有流体——request.bodyresponse.body的类型都是ReadableStream。当您转发它们时,body 会流过——块从发件人处接收并转发给最终的收件人,而无需在本地保留副本。因为不保留副本,流只能发送一次。

不过,您遇到的问题是,在将请求 body 流式传输到源服务器后,源服务器以 301、302、307 或 308 重定向响应。这些重定向要求客户端 re-transmit 向新的 URL 发出完全相同的请求(与 303 重定向不同,它指示客户端向新的 URL 发送 GET 请求)。但是,Cloudflare Workers 没有保留请求的副本 body,因此无法再次发送!

您会注意到,当您执行 fetch(event.request) 时,此问题不会发生,即使请求是 POST。原因是 event.requestredirect 属性 设置为 "manual",这意味着 fetch() 不会尝试自动跟随重定向。相反,fetch() 在这种情况下 returns 3xx 重定向响应本身并让应用程序处理它。如果您 return 对客户端浏览器的响应,浏览器将负责实际跟随重定向。

但是,在您的 worker 中,fetch() 似乎正在尝试自动遵循重定向,并产生错误。原因是你在构造 Request object:

时没有设置 redirect 属性
const init = {
  method: 'POST',
  headers: event.request.headers,
  body: event.request.body
};
// ...
await fetch(originUrl, init)

由于未设置 init.redirectfetch() 使用默认行为,与 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() 自动跟随重定向,那么您需要确保请求 body 是缓冲的而不是流式传输的,以便可以发送两次。为此,您需要将整个 body 读入字符串或 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)

回复说明

I would have thought this could be solved by simply copying the Response so that it's unused, like so:

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

如上所述,您看到的错误与此代码无关,但无论如何我想在这里评论两个问题以提供帮助。

首先,这行代码没有复制响应的所有属性。例如,您缺少 statusstatusText。还有一些 more-obscure 属性会在某些情况下出现(例如 webSocket,规范的 Cloudflare-specific 扩展)。

与其尝试列出每个 属性,我再次建议简单地传递旧的 Response object 本身作为选项结构:

new Response(response.body, response)

第二个问题是关于您对复制的评论。此代码复制 Response 的元数据,但 而不是 复制 body。那是因为 response.bodyReadableStream。此代码初始化新 Respnose object 以包含对相同 ReadableStream 的引用。一旦从该流中读取任何内容,该流就会被消耗 both Response objects.

通常这很好,因为通常您只需要一份回复。通常,您只是将其发送给客户。但是,在一些不寻常的情况下,您可能希望将响应发送到两个不同的地方。一个示例是使用 Cache API 来缓存响应的副本。您可以通过将整个 Response 读入内存来完成此操作,就像我们对上面的请求所做的那样。但是,对于 non-trivial 大小的响应,这可能会浪费内存并增加延迟(在将任何响应发送到客户端之前,您必须等待整个响应)。

相反,在这些不寻常的情况下,您真正​​想要做的是 "tee" 流,以便来自网络的每个块实际上写入两个不同的输出(如 Unix tee 命令,它来自管道中 T 形接头的想法)。

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

或者,作为快捷方式(当你不需要修改任何headers时),你可以这样写:

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

令人困惑的是,response.clone() 做的事情完全 不同于 new Response(response.body, response)response.clone() 响应 body,但保持 Headers 不变(如果它们在原始状态下是不可变的)。 new Response(response.body, response) 共享对同一 body 流的引用,但克隆了 headers 并使它们可变。我个人觉得这很令人困惑,但这是 Fetch API 标准指定的内容。