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。
默认情况下,从网络接收到的Request
和Response
objects有流体——request.body
和response.body
的类型都是ReadableStream
。当您转发它们时,body 会流过——块从发件人处接收并转发给最终的收件人,而无需在本地保留副本。因为不保留副本,流只能发送一次。
不过,您遇到的问题是,在将请求 body 流式传输到源服务器后,源服务器以 301、302、307 或 308 重定向响应。这些重定向要求客户端 re-transmit 向新的 URL 发出完全相同的请求(与 303 重定向不同,它指示客户端向新的 URL 发送 GET 请求)。但是,Cloudflare Workers 没有保留请求的副本 body,因此无法再次发送!
您会注意到,当您执行 fetch(event.request)
时,此问题不会发生,即使请求是 POST。原因是 event.request
的 redirect
属性 设置为 "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.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()
自动跟随重定向,那么您需要确保请求 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 })
如上所述,您看到的错误与此代码无关,但无论如何我想在这里评论两个问题以提供帮助。
首先,这行代码没有复制响应的所有属性。例如,您缺少 status
和 statusText
。还有一些 more-obscure 属性会在某些情况下出现(例如 webSocket
,规范的 Cloudflare-specific 扩展)。
与其尝试列出每个 属性,我再次建议简单地传递旧的 Response
object 本身作为选项结构:
new Response(response.body, response)
第二个问题是关于您对复制的评论。此代码复制 Response
的元数据,但 而不是 复制 body。那是因为 response.body
是 ReadableStream
。此代码初始化新 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 标准指定的内容。
我正在尝试使用 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。
默认情况下,从网络接收到的Request
和Response
objects有流体——request.body
和response.body
的类型都是ReadableStream
。当您转发它们时,body 会流过——块从发件人处接收并转发给最终的收件人,而无需在本地保留副本。因为不保留副本,流只能发送一次。
不过,您遇到的问题是,在将请求 body 流式传输到源服务器后,源服务器以 301、302、307 或 308 重定向响应。这些重定向要求客户端 re-transmit 向新的 URL 发出完全相同的请求(与 303 重定向不同,它指示客户端向新的 URL 发送 GET 请求)。但是,Cloudflare Workers 没有保留请求的副本 body,因此无法再次发送!
您会注意到,当您执行 fetch(event.request)
时,此问题不会发生,即使请求是 POST。原因是 event.request
的 redirect
属性 设置为 "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.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()
自动跟随重定向,那么您需要确保请求 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 })
如上所述,您看到的错误与此代码无关,但无论如何我想在这里评论两个问题以提供帮助。
首先,这行代码没有复制响应的所有属性。例如,您缺少 status
和 statusText
。还有一些 more-obscure 属性会在某些情况下出现(例如 webSocket
,规范的 Cloudflare-specific 扩展)。
与其尝试列出每个 属性,我再次建议简单地传递旧的 Response
object 本身作为选项结构:
new Response(response.body, response)
第二个问题是关于您对复制的评论。此代码复制 Response
的元数据,但 而不是 复制 body。那是因为 response.body
是 ReadableStream
。此代码初始化新 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 标准指定的内容。