3

我正在尝试使用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.

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

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

1 回答 1

12

这里有几个问题。

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

默认情况下,RequestResponse网络接收到的对象都有流式传输体——request.body并且response.body两者都有 type ReadableStream。当您继续转发它们时,正文会流过——从发件人那里接收到块并转发给最终的收件人,而无需在本地保留副本。因为没有保留副本,所以流只能发送一次。

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

您会注意到fetch(event.request),即使请求是 POST,也不会发生此问题。原因是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.requestinit 结构:

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

这是有效的,因为 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)

关于响应的说明

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

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

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

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

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

new Response(response.body, response)

第二个问题是您对复制的评论。此代码复制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)

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

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

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

于 2019-05-02T18:14:36.530 回答