Subscribe to receive notifications of new posts:

How we found a bug in the hyper HTTP library我们如何在hyper HTTP库中发现一个bug

2026-06-222026-06-22

12 min read12分钟阅读

The Images service, built in Rust on Workers, runs on every machine in Cloudflare’s edge network. To handle client connections, we use hyper, an open-source HTTP library for Rust.Images服务使用Rust构建,运行在Workers上,部署于Cloudflare边缘网络的每台机器上。为了处理客户端连接,我们使用了hyper,一个Rust的开源HTTP库。

Last year, we introduced the Images binding to enable custom, programmatic workflows for processing remote images in Workers. At the end of 2025, we rearchitected the binding to provide a more direct, local connection between the Workers runtime and the Images service.去年,我们引入了Images绑定,以便在Workers中实现自定义、可编程的远程图像处理工作流。2025年底,我们重新架构了该绑定,以在Workers运行时和Images服务之间提供更直接、更本地的连接。

Shortly after rollout, we received reports that transformation requests from the binding were failing — but only intermittently and only for larger images. Even stranger, the responses for these requests returned a 200 status without any errors logged. The image data was simply cut short: A response that should have been two megabytes might arrive with a few hundred kilobytes instead.上线后不久,我们收到报告称来自绑定的转换请求失败——但只是间歇性地发生,且仅针对较大的图像。更奇怪的是,这些请求的响应返回了200状态码,且没有任何错误日志。图像数据只是被截断了:本应为两兆字节的响应可能只到达了几百千字节。

We spent six weeks chasing a nearly invisible bug — a race condition that occurred only under specific conditions — in the hyper library that impacted how the Images binding returned processed image data back to the client. In the end, it took four lines of code to fix it.我们花了六周时间追踪一个几乎看不见的bug——一个仅在特定条件下发生的竞态条件——它存在于hyper库中,影响了Images绑定将处理后的图像数据返回给客户端的方式。最终,修复它只用了四行代码。

Hops, handoffs, and hyper跳转、交接与hyper

When developers build on Cloudflare, they compose full-stack applications from a set of platform services that are accessible to Workers through bindings. Bindings provide direct APIs to resources on the Developer Platform like compute, storage, AI inference, and media processing.当开发者在Cloudflare上构建时,他们通过绑定将全栈应用组合成一组平台服务,Workers可以通过这些绑定访问资源。绑定为开发者平台上的计算、存储、AI推理和媒体处理等资源提供了直接API。

The Images binding decouples image optimization from delivery; you can transcode, composite, or manipulate images without needing to return the output as an HTTP response. It also lets you apply optimization parameters in any order, rather than following the fixed sequence imposed by the URL interface. Here, a worker can pass image data directly to the Images API, chain operations together, and get the processed result back as a stream:Images绑定将图像优化与交付解耦;你可以转码、合成或操作图像,而无需将输出作为HTTP响应返回。它还允许你以任意顺序应用优化参数,而不是遵循URL接口强加的固定顺序。这里,一个worker可以直接将图像数据传递给Images API,将操作链接在一起,并将处理后的结果作为流返回:

const result = await env.IMAGES
  .input(image)
  .transform({ width: 800, rotate: 90 })
  .output({ format: "image/avif" });
return result.response();

At a high level, this is how image data moves through our various services:在高层面上,图像数据通过我们的各种服务移动如下:

The pipe represents a socket connection between the intermediary and Images, where data is handed off from one process to the next through the kernel’s buffer.管道代表中介与Images之间的套接字连接,数据通过内核缓冲区从一个进程传递到下一个进程。

The binding communicates with Images through a socket connection managed by the Workers runtime. A socket connection is a communication channel between two processes. Each end of the socket has buffers that are managed by the operating system’s kernel; these buffers are temporary holding areas where data sits after one side writes it but before the other side reads it.绑定通过Workers运行时管理的套接字连接与Images通信。套接字连接是两个进程之间的通信通道。套接字的每一端都有由操作系统内核管理的缓冲区;这些缓冲区是临时存储区域,数据在一方写入后、另一方读取前暂存于此。

Hyper manages the connection on the Images service’s side, reading incoming requests from the socket and writing responses back to it.Hyper在Images服务端管理连接,从套接字读取传入请求并将响应写回套接字。

When a request uses the Images binding, the Images service reads the input, performs the requested optimization operations, and encodes the result. It then passes the entire encoded image to hyper as a single in-memory block. 当请求使用Images绑定时,Images服务读取输入,执行请求的优化操作,并对结果进行编码。然后它将整个编码后的图像作为单个内存块传递给hyper。

Hyper writes this response data into its own internal buffer. At this point, hyper considers the encoding work as complete, since it has all the bytes that it needs to send. The next step is to flush its internal buffer to the socket’s outbound buffer, moving the data from the Images service to the intermediary on the other end.Hyper将此响应数据写入其内部缓冲区。此时,hyper认为编码工作已完成,因为它已拥有所有需要发送的字节。下一步是将其内部缓冲区刷新到套接字的出站缓冲区,将数据从Images服务移动到另一端的中间件。

If the reader on the other end is fast, then hyper can flush everything in one pass — the outbound buffer will have room because the reader is consuming data as quickly as it arrives. Once all data is sent, hyper issues a shutdown on the socket, signaling that the connection is finished and no more data will be written. But if the reader is slower (even by a few milliseconds), then the outbound buffer fills up, and hyper needs to wait until there’s room to continue writing.如果另一端的读取器速度很快,那么hyper可以一次性刷新所有数据——出站缓冲区将有空间,因为读取器在数据到达时立即消费。一旦所有数据发送完毕,hyper会在套接字上发出关闭信号,表示连接已完成,不再写入数据。但如果读取器较慢(即使慢几毫秒),出站缓冲区会填满,hyper需要等待有空间后再继续写入。

Taking the local本地化

All incoming traffic on Cloudflare's network passes through FL, an internal intermediary service that runs security and performance features and routes requests to the appropriate backend. When we first launched the binding, image data flowed from the Workers runtime, through FL, to the Images service.所有进入Cloudflare网络的流量都经过FL,一个内部中介服务,它运行安全和性能功能,并将请求路由到相应的后端。当我们首次推出绑定时,图像数据从Workers运行时流经FL,再到Images服务。

This path was a natural fit for our initial release and follows the same architecture as our URL interface. Over time, though, this coupling with FL became a constraint: Every change to the binding had to follow FL’s release cycle.这条路径自然适合我们的初始发布,并遵循与URL接口相同的架构。但随着时间的推移,这种与FL的耦合变成了一个约束:对绑定的每次更改都必须遵循FL的发布周期。

In December 2025, the Images team replaced FL with a new intermediary service, an internal worker binding that runs on the same machine. In the original architecture, data moved through FL over network sockets; this path carried the overhead of FL’s full processing pipeline, such as DNS lookups and routing.2025年12月,Images团队用一个新的中介服务替换了FL,这是一个在同一台机器上运行的内部worker绑定。在原始架构中,数据通过网络套接字经过FL;这条路径带来了FL完整处理管道的开销,例如DNS查找和路由。

The internal binding replaced these with Unix sockets to directly connect the services on the same machine, bypassing FL and the overhead of the network stack. This made the request path to Images faster and gave the team independent control over binding releases.内部绑定用Unix套接字替换了这些,以直接连接同一台机器上的服务,绕过了FL和网络栈的开销。这使得通往Images的请求路径更快,并让团队能够独立控制绑定的发布。

Within days of the rollout, we received our first customer report.上线几天内,我们收到了第一份客户报告。

200 OK (not OK)200 OK(并不OK)

The first sign of trouble came from a customer with a non-standard setup: two layers of image processing, where one pipeline was nested inside another.第一个麻烦迹象来自一个具有非标准设置的客户:两层图像处理,其中一个管道嵌套在另一个管道内。

First, their worker used the Images binding to composite multiple large source images from R2 — a JPEG background plus PNG overlay layers — into a single combined JPEG. Second, they further compressed, transcoded, and resized the result through the URL interface.首先,他们的worker使用Images绑定将来自R2的多个大型源图像——一个JPEG背景加上PNG叠加层——合成一个组合的JPEG。其次,他们通过URL接口进一步压缩、转码和调整结果的大小。

The bug originated in the inner pipeline’s return path, where the response was truncated before reaching the outer pipeline.该bug源于内部管道的返回路径,响应在到达外部管道之前被截断。

The inner pipeline (transformation binding) handled compositing. The outer pipeline (transformation URL) handled delivery optimizations like scaling and format conversion. This layered approach meant that when the inner pipeline silently returned a truncated response, the only visible error appeared one level up:内部管道(转换绑定)处理合成。外部管道(转换URL)处理交付优化,如缩放和格式转换。这种分层方法意味着,当内部管道静默返回截断的响应时,唯一可见的错误出现在上一层:

error reading a body from connection: end of file before message length reached

The outer pipeline received HTTP 200 from the inner one, with a Content-Length header that promised several megabytes. The actual body was only a fraction of that: In one request, only ~200 KB arrived out of an expected 3.3 MB. The error surfaced in the outer pipeline, but the truncation could have originated in the binding, the intermediary service, the Images service, or somewhere in between.外部管道从内部管道收到HTTP 200,其Content-Length标头承诺了几兆字节。实际主体只有其中的一小部分:在一个请求中,预期的3.3 MB只到达了约200 KB。错误出现在外部管道,但截断可能源于绑定、中介服务、Images服务或两者之间的某个环节。

When a browser receives a truncated image, the result is visible. Depending on the format, the image either renders partially (e.g., with the bottom half missing or gray) or fails to decode entirely, instead displaying a broken image.当浏览器收到截断的图像时,结果是可见的。根据格式不同,图像要么部分渲染(例如,下半部分缺失或变灰),要么完全无法解码,显示为损坏的图像。

Debugging in the dark在黑暗中调试

From here, we worked inward through the request path, testing each layer to isolate where the truncation was happening. Some of these efforts hit dead ends; others left breadcrumbs that narrowed the search:从这里开始,我们沿着请求路径向内工作,测试每一层以隔离截断发生的位置。其中一些努力走进了死胡同;另一些则留下了缩小搜索范围的线索:

  • Building a reproduction. We built a worker that mimicked the customer’s nested setup, then stripped away layers until we could trigger the bug with the binding alone. A small script let us fire requests in batches. In one early run, 19 out of 25 requests failed. The amount of data that did arrive — roughly 200 KB — was suspiciously close to the size of the socket buffer in production. This confirmed that the problem wasn’t tied to the customer’s configuration and gave us a reliable way to trigger the bug on demand.构建复现。我们构建了一个模仿客户嵌套设置的worker,然后逐层剥离,直到仅用绑定就能触发bug。一个小脚本让我们可以批量发送请求。在一次早期运行中,25个请求中有19个失败。实际到达的数据量——大约200 KB——与生产环境中套接字缓冲区的大小惊人地接近。这证实了问题与客户的配置无关,并给了我们一种按需触发bug的可靠方法。

  • Investigating timeouts. Early on, we suspected the truncation might be related to timeout behavior (i.e., the connection was being closed after a time limit). This theory didn’t hold, as the truncation wasn’t correlated with request duration.调查超时。早期,我们怀疑截断可能与超时行为有关(即连接在时间限制后被关闭)。这个理论不成立,因为截断与请求持续时间无关。

  • Updating hyper version. When the bug was first reported, we were running 0.14.x, while the latest hyper version was around 1.8.x. We tested across hyper versions 0.14, 1.7, and 1.8, just in case the most obvious answer was the correct (and easiest) one. But the bug appeared in each version, which meant that there wasn’t an upstream fix.更新hyper版本。当bug首次报告时,我们运行的是0.14.x,而最新的hyper版本大约是1.8.x。我们在hyper版本0.14、1.7和1.8上进行了测试,以防最明显的答案是正确的(也是最简单的)。但bug在每个版本中都出现,这意味着上游没有修复。

  • Reproducing locally. We ran local integration tests on macOS and a Debian VM. Even under considerable load, our local requests never triggered any failure. Making direct curl requests to the binding socket and replaying captured requests always seemed to work. The bug only appeared on the full production path when there was real concurrency and a real Workers runtime client on the other end of the socket. This led us to suspect the runtime itself.本地复现。我们在macOS和Debian虚拟机上运行了本地集成测试。即使在相当大的负载下,我们的本地请求也从未触发任何失败。直接对绑定套接字进行curl请求并重放捕获的请求似乎总是有效。该bug只出现在完整的生产路径上,当存在真正的并发性和套接字另一端的真实Workers运行时客户端时。这让我们怀疑运行时本身。

  • Ruling out the Workers runtime. We examined the HTTP client that the Workers runtime uses to communicate with Images through the binding socket. None of the traces from either side of the connection showed any syscalls that indicated an unexpected close or early termination. We observed that the client behaved correctly and multiple other services used the same client without issues.排除Workers运行时。我们检查了Workers运行时用于通过绑定套接字与Images通信的HTTP客户端。连接两端的跟踪都没有显示任何指示意外关闭或提前终止的系统调用。我们观察到客户端行为正确,并且多个其他服务使用相同的客户端而没有问题。

  • Distributed tracing. By inspecting request traces end-to-end, we confirmed that the truncated body was already present before it reached the outer transformation layer in the customer’s setup. That narrowed the problem to the inner pipeline — the binding path through the Images service.分布式追踪。通过端到端检查请求跟踪,我们确认截断的主体在到达客户设置中的外部转换层之前就已经存在。这将问题缩小到内部管道——通过Images服务的绑定路径。

  • Instrumenting the intermediary service. We added instrumentation to the intermediary service to measure body sizes before forwarding the response data. The bodies were already truncated by the time they left the Images service, so the intermediary was ruled out.对中介服务进行插桩。我们在中介服务中添加了插桩,以在转发响应数据之前测量主体大小。主体在离开Images服务时已经被截断,因此排除了中介。

  • Deeper tracing within the Images service. At the service level, the request was processed, the image was properly encoded, and the response was sent with HTTP 200.在Images服务内进行更深入的追踪。在服务层面,请求被处理,图像被正确编码,并且响应以HTTP 200发送。

The only consistent signal was that the bug was timing-dependent: It appeared only on the production path, with real concurrency, and only for larger images.唯一一致的信号是bug与时间相关:它只出现在生产路径上,具有真正的并发性,并且只针对较大的图像。

A kernel of truth真相的内核

Tools for application-level debugging told only what the system thought it was doing. But according to the system, everything was fine: Tracing said the response was sent; logging reported no errors, and the Images service returned 200 on every request.应用级调试工具只告诉我们系统认为自己在做什么。但根据系统,一切正常:追踪显示响应已发送;日志报告没有错误,Images服务在每个请求上都返回200。

To see what the system was actually doing, we attached strace to the Images service. strace records the syscalls that a process makes to the kernel, which could show us exactly which bytes were written, when a shutdown was called, and whether the client sent any termination signal.为了查看系统实际在做什么,我们将strace附加到Images服务。strace记录进程向内核发出的系统调用,这可以精确显示哪些字节被写入、何时调用了关闭,以及客户端是否发送了任何终止信号。

Setting up the trace was delicate. strace works by intercepting syscalls as they happen, which adds a small amount of timing overhead to each one. Filtering for a narrow set of syscalls kept that overhead minimal. Broadening the filter, however, slowed the process just enough to shift the timing between the flush and the shutdown check — and make the bug disappear entirely. That alone reinforced our theory that the issue was timing-sensitive.设置追踪需要小心。strace通过拦截系统调用来工作,这会给每个系统调用增加少量定时开销。将过滤器限制在一组狭窄的系统调用上可以保持这种开销最小。然而,扩大过滤器会稍微减慢进程,足以改变刷新和关闭检查之间的时间——并使bug完全消失。这本身就强化了我们的理论,即问题对时间敏感。

Using a reproduction worker, we triggered the bug and compared the syscall output between successful and failing requests.使用复现worker,我们触发了bug,并比较了成功和失败请求之间的系统调用输出。

In a successful request, the response is written in chunks as the socket buffer allows, with shutdown called only after all the data is sent. For example, this may look like:在成功请求中,响应以块的形式写入,具体取决于套接字缓冲区的允许,只有在所有数据发送后才调用关闭。例如,这可能看起来像:

sendto(42, "HTTP/1.1 200 OK\r\nContent-Length: 14991808\r\n...", ...) = 219264
sendto(42, "\xff\xd8\xff\xe0...", 292352) = 292352
// ... keeps writing until buffer drains ...
sendto(42, "...", 292352) = 292352
shutdown(42, SHUT_WR) = 0

When we reproduced the bug, a failing request looked like:当我们复现bug时,失败请求看起来像:

sendto(42, "HTTP/1.1 200 OK\r\nContent-Length: 14991808\r\n...", ...) = 219264
shutdown(42, SHUT_WR) = 0

Here, there is only one write — just enough for the headers and a sliver of the body — before the shutdown is immediately called. Out of a 14.9 MB response, only about 219 KB was sent. The remaining ~14.8 MB of image data never left hyper’s internal buffer, nor was there any termination signal from the client between the write and the shutdown. Instead, the Images service prematurely shut down the connection on its own, genuinely believing it was finished.这里只有一次写入——刚好够写入头部和一小部分主体——然后立即调用关闭。在14.9 MB的响应中,只发送了大约219 KB。剩余的约14.8 MB图像数据从未离开hyper的内部缓冲区,客户端在写入和关闭之间也没有发送任何终止信号。相反,Images服务过早地自行关闭了连接,真心认为它已经完成了。

The failing requests confirmed that the bug was a race condition that triggered intermittently. Whether a request succeeded or failed depended on whether the flush and shutdown operations overlapped, which changed from request to request. When the buffer was still full at the exact moment that hyper decided the connection was finished, data was lost.失败的请求证实了该bug是一个间歇性触发的竞态条件。请求成功还是失败取决于刷新和关闭操作是否重叠,这因请求而异。当缓冲区在hyper决定连接完成的确切时刻仍然满时,数据就会丢失。

When the reader consumes slower than hyper writes, the outbound buffer fills up. If hyper shuts down the connection before the buffer drains, then only a fraction of the response makes it to the intermediary; this incomplete data gets forwarded back to the Workers runtime and the client.当读取器消费速度慢于hyper写入速度时,出站缓冲区会填满。如果hyper在缓冲区排空之前关闭连接,那么只有一小部分响应到达中介;这不完整的数据会被转发回Workers运行时和客户端。

The December rearchitecture didn't introduce this bug, which had been present in hyper for years across multiple major versions. But the new intermediary changed who was reading on the response side of the socket. Our working theory is that FL, the previous intermediary, consumed data fast enough that the socket buffer rarely filled during a response. The new reader read at a pace that occasionally let the buffer fill during larger responses.12月的重新架构并没有引入这个bug,它已经在hyper中存在多年,跨越多个主要版本。但新的中介改变了谁在套接字的响应端进行读取。我们的工作理论是,之前的中介FL消费数据足够快,以至于在响应期间套接字缓冲区很少填满。新的读取器以某种速度读取,偶尔会在较大响应期间让缓冲区填满。

These few milliseconds of backpressure, introduced by an improvement that made everything else faster, were all it took to surface a flaw that had been hiding in plain sight.这几毫秒的反压,由一个让其他一切变得更快改进引入,就足以暴露一个一直隐藏的缺陷。

Inside the dispatch loop在调度循环内部

Hyper's HTTP/1 connection lifecycle is driven by a state machine in a file called dispatch.rs. It runs a loop that reads requests, writes responses, flushes the write buffer to the socket, and decides when to shut down. In simplified form:Hyper的HTTP/1连接生命周期由一个位于dispatch.rs文件中的状态机驱动。它运行一个循环,读取请求、写入响应、将写入缓冲区刷新到套接字,并决定何时关闭。简化形式如下:

fn poll_loop(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
    loop {
        let _ = self.poll_read(cx)?;
        let _ = self.poll_write(cx)?;
        let _ = self.poll_flush(cx)?;

        if !self.conn.wants_read_again() {
            return Poll::Ready(Ok(()));
        }
    }
}

More precisely, the let _ before poll_flush is where the bug lives.更准确地说,bug位于poll_flush之前的let _。

In Rust, let _ = expr discards the expression's result, including Poll::Pending, the signal that the flush isn’t done yet. The flush might still have megabytes sitting in its buffer, but the loop never finds out.在Rust中,let _ = expr会丢弃表达式的结果,包括Poll::Pending,即刷新尚未完成的信号。刷新可能仍有兆字节的数据在其缓冲区中,但循环永远不会知道。

When a request fails, this is the exact sequence of events:当请求失败时,这是确切的事件序列:

  1. The Images service finishes encoding the image and hands the entire response to hyper as a single in-memory block.Images服务完成图像编码,并将整个响应作为单个内存块交给hyper。

  2. Hyper writes the block into its internal buffer and marks its write state as Writing::Closed. From an encoding standpoint, the work is done — there is nothing left to encode.Hyper将块写入其内部缓冲区,并将其写入状态标记为Writing::Closed。从编码的角度来看,工作已完成——没有剩余内容需要编码。

  3. Hyper calls poll_flush to move the buffered data to the socket. In our previous example, the socket accepted about 219 KB. The remaining ~14.8 MB stays in hyper's buffer. The socket is full, so the kernel returns Poll::Pending.Hyper调用poll_flush将缓冲数据移动到套接字。在我们之前的示例中,套接字接受了大约219 KB。剩余的约14.8 MB留在hyper的缓冲区中。套接字已满,因此内核返回Poll::Pending。

  4. poll_loop discards the Poll::Pending with let _.poll_loop使用let _丢弃了Poll::Pending。

  5. It checks wants_read_again(). The full request was already received, so this returns false.它检查wants_read_again()。完整的请求已经收到,因此返回false。

  6. poll_loop returns Poll::Ready(Ok(())), signaling that the loop is finished, even though the flush is not.poll_loop返回Poll::Ready(Ok(())),表示循环已完成,即使刷新尚未完成。

  7. poll_shutdown() fires. The SHUT_WR syscall is issued.poll_shutdown()触发。发出SHUT_WR系统调用。

  8. The client receives 219 KB and an EOF (end-of-file) indicating that the connection is closed, even though it expects 14.9 MB.客户端收到219 KB和一个EOF(文件结束符),表示连接已关闭,尽管它期望14.9 MB。

In the second step, hyper marks the write operation as complete as soon as the response body is buffered (i.e., when encoding is finished), rather than when it has actually been flushed. Most of the time, the flush completes in a single pass and this distinction is invisible. On the rare occasions when the socket buffer is full, the flush has to wait — even though hyper doesn't. The bytes are still sitting in hyper’s buffer, waiting to be flushed to the socket. Hyper proceeds to shut down the connection with this data still in the buffer.在第二步中,hyper在响应体被缓冲时(即编码完成时)就将写操作标记为完成,而不是在实际刷新完成时。大多数情况下,刷新在一次传递中完成,这种区别是不可见的。在极少数套接字缓冲区已满的情况下,刷新必须等待——即使hyper不等待。字节仍然在hyper的缓冲区中,等待被刷新到套接字。Hyper在数据仍在缓冲区中时继续关闭连接。

This also explains why curl never triggered the bug. Curl reads data as fast as it arrives: The socket buffer never fills, the flush always completes immediately, and the discarded return value is harmless. The production path, with a reader that occasionally paused for a few milliseconds, was the only configuration where the buffer filled at exactly the wrong moment.这也解释了为什么curl从未触发该bug。Curl在数据到达时立即读取:套接字缓冲区永远不会填满,刷新总是立即完成,丢弃的返回值是无害的。生产路径中有一个偶尔暂停几毫秒的读取器,是唯一让缓冲区在错误时刻填满的配置。

Don’t forget to flush别忘了刷新

After weeks of investigation, the fix itself was conceptually simple. Hyper needed to check whether the flush was actually done before moving on.经过数周的调查,修复本身在概念上很简单。Hyper需要在继续之前检查刷新是否实际完成。

Our reproduction worker confirmed that the bug existed, but it couldn't tell us why a given request failed. Before writing the fix, we needed a test that could trigger the exact socket conditions inside hyper.我们的复现worker确认了bug的存在,但它无法告诉我们为什么特定请求失败。在编写修复之前,我们需要一个能够触发hyper内部确切套接字条件的测试。

We knew the conditions that triggered the bug: a socket that accepts one chunk of data and then blocks. To test with a controlled scenario, we built a custom wrapper around a TCP stream that simulated a full socket buffer. The wrapper accepted 8 KB on the first write, then returned Poll::Pending on every subsequent write, mimicking a reader that stopped draining the buffer.我们知道触发bug的条件:一个接受一块数据然后阻塞的套接字。为了在受控场景中进行测试,我们构建了一个围绕TCP流的自定义包装器,模拟了满的套接字缓冲区。该包装器在第一次写入时接受8 KB,然后在每次后续写入时返回Poll::Pending,模拟一个停止排空缓冲区的读取器。

The test sent a 500 KB response through this constrained socket and checked whether hyper called shutdown while 492 KB was still buffered. Without a fix, it did. With the fix, it waited.测试通过这个受限套接字发送了500 KB的响应,并检查hyper是否在仍有492 KB缓冲时调用了关闭。没有修复时,它确实调用了。有了修复,它等待了。

Initially, we applied the fix in hyper’s dispatch loop. Instead of discarding the result of poll_flush, we checked to see whether the flush was actually done:最初,我们在hyper的调度循环中应用了修复。我们没有丢弃poll_flush的结果,而是检查刷新是否实际完成:

let flush_result = self.poll_flush(cx)?;

if flush_result.is_pending() {
    return Poll::Pending;
}

if !self.conn.wants_read_again() {
    return Poll::Ready(Ok(()));
}

If the flush hasn't completed, then the loop returns Poll::Pending to the asynchronous runtime. The runtime waits for the socket to become writable, then wakes the task back up to continue the flush. The connection shuts down only after all data has been sent.如果刷新尚未完成,则循环向异步运行时返回Poll::Pending。运行时等待套接字变为可写,然后唤醒任务以继续刷新。只有在所有数据发送完毕后,连接才会关闭。

When we deployed this fix, we observed that every byte was written and the shutdown was called only after the buffer was actually empty. The customer who made the first report also confirmed that the issue disappeared.当我们部署此修复时,我们观察到每个字节都被写入,并且只有在缓冲区实际为空时才调用关闭。第一个报告的客户也确认问题消失了。

While our initial solution worked, the dispatch loop wasn’t the right place for the fix. Returning Poll::Pending early could slow down other operations on the same connection by reducing how frequently reads are polled, causing unintended backpressure. It also doesn't correctly handle keepalive connections, where a single connection handles multiple requests in sequence — these should remain reusable even while the previous response is still being flushed. Neither issue affected our particular service (where keepalive is disabled), but both could affect other hyper users if the fix were contributed upstream.虽然我们的初始解决方案有效,但调度循环并不是修复的正确位置。过早返回Poll::Pending可能会通过减少读取轮询的频率来减慢同一连接上的其他操作,导致意外的反压。它也不能正确处理keepalive连接,其中单个连接按顺序处理多个请求——这些连接即使在之前的响应仍在刷新时也应保持可重用。这两个问题都没有影响我们的特定服务(其中keepalive被禁用),但如果将修复贡献到上游,可能会影响其他hyper用户。

We traced through hyper's connection lifecycle and found a more targeted approach. Rather than changing how the dispatch loop behaves, we applied the fix at the point where shutdown is actually called. Before shutting down the socket, hyper should first flush any remaining data in its buffer:我们追踪了hyper的连接生命周期,并找到了一个更有针对性的方法。我们没有改变调度循环的行为,而是在实际调用关闭的点应用了修复。在关闭套接字之前,hyper应该首先刷新其缓冲区中的任何剩余数据:

pub(crate) fn poll_shutdown(
    &mut self,
    cx: &mut Context<'_>,
) -> Poll<io::Result<()>> {
    ready!(self.poll_flush(cx)?);
    Pin::new(&mut self.io).poll_shutdown(cx)
}

This leaves the dispatch loop unchanged. It adds a flush only at the exact point where data loss would otherwise occur — the moment before shutdown.这保持了调度循环不变。它只在数据丢失可能发生的精确点——关闭之前——添加了一个刷新。

What stayed with us我们的收获

None of the tools at the application level surfaced any errors, crashes, or log entries that provided useful clues. Application-level observability can have a blind spot for bugs that live below its awareness.应用层面的工具没有提供任何错误、崩溃或日志条目来提供有用的线索。应用级可观测性对于存在于其意识之下的bug可能存在盲点。

The failure occurred intermittently, scaled with response size, couldn’t be reproduced with simple tools like curl, and disappeared when we observed the system more closely. These signals pointed to a timing-dependent bug in the connection layer, not in the application logic.故障间歇性发生,随响应大小扩展,无法用curl等简单工具复现,并且在更密切观察系统时消失。这些信号指向连接层中一个与时间相关的bug,而不是应用逻辑。

Our breakthrough came from using kernel-level tooling with strace, the one layer that records what actually happened on the socket. The underlying bug lived in the few milliseconds between a partial flush and a premature shutdown — a window that opened only after we made the system faster.我们的突破来自使用内核级工具strace,这是记录套接字上实际发生情况的唯一一层。底层bug存在于部分刷新和过早关闭之间的几毫秒窗口内——这个窗口只有在我们使系统更快后才打开。

We merged our fix and the deterministic test into hyperium/hyper via PR #4018. It will be available in a future hyper release, ensuring that any service using hyper’s HTTP/1 implementation won’t lose response data to the same race condition.我们通过PR #4018将修复和确定性测试合并到hyperium/hyper中。它将在未来的hyper版本中可用,确保任何使用hyper的HTTP/1实现的服务不会因相同的竞态条件而丢失响应数据。

In the meantime, we’re running an internal fork with the patch applied. This fix stabilized the binding’s architecture, creating a reliable foundation to expand its functionality.与此同时,我们正在运行一个应用了补丁的内部分支。此修复稳定了绑定的架构,为扩展其功能创建了可靠的基础。

The Images binding initially covered only transformations of remote images. Earlier this month, we announced that the Images binding now supports operations for hosted images, giving developers a unified way to build media-rich applications on Cloudflare.Images绑定最初只涵盖远程图像的转换。本月早些时候,我们宣布Images绑定现在支持托管图像的操作,为开发者在Cloudflare上构建富媒体应用提供了统一的方式。

Read more about how the binding works in our documentation.在我们的文档中阅读更多关于绑定如何工作的信息。

Image Optimization图像优化Cloudflare ImagesCloudflare ImagesDevelopers开发者Developer Platform开发者平台Cloudflare WorkersCloudflare WorkersOpen Source开源

Follow on X关注X

Cloudflare|@cloudflare

Related posts相关文章

June 25, 20262026年6月25日

How we built saga rollbacks for Cloudflare Workflows

我们如何为Cloudflare Workflows构建了saga回滚

Cloudflare Workflows, our durable execution engine for multi-step applications, now supports saga-style rollbacks, allowing developers to specify a compensating action for each step.do(). ...Cloudflare Workflows,我们的多步骤应用持久执行引擎,现在支持saga风格的回滚,允许开发者为每个step.do()指定补偿操作。...

Workflows, Workflows,Cloudflare Workers, Cloudflare Workers,Developers 

June 24, 20262026年6月24日

Unlocking the Cloudflare app ecosystem with OAuth for all

通过面向所有人的OAuth解锁Cloudflare应用生态系统

Self-Managed OAuth is now available to all developers on Cloudflare. Here's how we executed a zero-downtime migration of our core OAuth engine to make it happen....自管理OAuth现已对所有Cloudflare开发者可用。以下是我们如何执行核心OAuth引擎的零停机迁移以实现这一目标。...

Developers, 开发者,API, API,Security, 安全,OAuth, OAuth,Developer Platform, 开发者平台,Agents, 代理,Product News, 产品新闻,Cloudflare Media Platform, Cloudflare媒体平台,Identity 身份

June 19, 20262026年6月19日

Temporary Cloudflare Accounts for AI agents

面向AI代理的临时Cloudflare账户

The moment an agent needs to deploy something, it slams face-first into a wall built for humans. Today we're rolling out Temporary Accounts on Cloudflare Workers. Any agent can now run wrangler deploy — temporary and get a live Worker in seconds....当代理需要部署某些东西时,它会一头撞上为人类构建的墙。今天我们在Cloudflare Workers上推出了临时账户。任何代理现在都可以运行wrangler deploy --temporary,并在几秒钟内获得一个实时Worker。...

June 18, 20262026年6月18日

Bringing more agent harnesses and frameworks to Cloudflare, starting with Flue

将更多代理框架和工具引入Cloudflare,从Flue开始

The Agents SDK is now a runtime any agent framework can build on. Today we're opening up the Agents SDK primitives, with Flue as a first framework targeting Agents SDK, and rolling out agents in the dashboard....Agents SDK现在是一个任何代理框架都可以构建的运行时。今天我们开放了Agents SDK原语,Flue作为首个针对Agents SDK的框架,并在仪表板中推出了代理。...