Cloudflare Worker TypeError:一次性使用的主体

Nic*_*ick 3 javascript fetch-api cloudflare-workers

我正在尝试使用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)
  }
}
Run Code Online (Sandbox Code Playgroud)

Ken*_*rda 8

这里有几个问题。

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

默认情况下,RequestResponse从网络接收的对象具有流媒体机构-request.bodyresponse.body都有型ReadableStream。当你转发它们时,正文流通过——从发件人处接收块并转发给最终的收件人,而无需在本地保留副本。由于不保留副本,因此该流只能发送一次。

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

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

但是,在您的工作人员中,它似乎fetch()试图自动跟随重定向,并产生错误。原因是您redirect在构造Request对象时没有设置属性:

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

由于init.redirect未设置,因此fetch()使用默认行为,与 相同redirect = "automatic",即fetch()尝试跟随重定向。如果您想fetch()使用手动重定向行为,您可以添加redirect: "manual"init. 但是,看起来您在这里真正想做的是复制整个请求。在这种情况下,您应该只传递event.requestinit 结构的位置:

// Copy all properties from event.request *except* URL.
await fetch(originUrl, event.request);
Run Code Online (Sandbox Code Playgroud)

这是有效的,因为 aRequest具有fetch()第二个参数想要的所有字段。

如果您想要自动重定向怎么办?

如果你真的想fetch()自动跟随重定向,那么你需要确保请求正文被缓冲而不是流式传输,以便它可以发送两次。为此,您需要将整个正文读入一个字符串 or 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)
Run Code Online (Sandbox Code Playgroud)

关于回复的说明

我原以为这可以通过简单地复制 Response 来解决,以便它不被使用,如下所示:

return new Response(response.body, { headers: response.headers })
Run Code Online (Sandbox Code Playgroud)

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

首先,这行代码不会复制响应的所有属性。例如,您缺少statusstatusText。在某些情况下(例如webSocket,特定于 Cloudflare 的规范扩展)还会出现一些更模糊的属性。

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

new Response(response.body, response)
Run Code Online (Sandbox Code Playgroud)

第二个问题是关于复制的评论。这段代码复制Response的元数据,但并没有复制体。那是因为response.body是一个ReadableStream. 此代码初始化新Respnose对象以包含对相同ReadableStream. 一旦从该流中读取任何内容,两个 Response对象都会消耗该流。

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

相反,在这些不寻常的情况下,你真正想做的是“tee”流,这样来自网络的每个块实际上都被写入两个不同的输出(如 Unixtee命令,它来自 T 连接的想法在管道中)。

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

或者,作为快捷方式(当您不需要修改任何标题时),您可以编写:

// ONLY use this when there are TWO destinations for the
// response body!
response.clone()
Run Code Online (Sandbox Code Playgroud)

令人困惑的是,response.clone()做的东西完全不同new Response(response.body, response)response.clone()tee 响应主体,但保持 Headers 不可变(如果它们在原始上是不可变的)。new Response(response.body, response)共享对同一主体流的引用,但会克隆标头并使它们可变。我个人觉得这很令人困惑,但这是 Fetch API 标准指定的。