<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/the-case-of-the-readable-dead-connection-a-ruby-mystery-smy8cygd3" -->

---
title: The Case of the Readable Dead Connection: A Ruby Mystery
description: A detective-story-style postmortem explaining how a dead persistent HTTPS connection could pass a viability check and be handed to a new non-idempotent POST...
canonical: https://daily.dev/posts/the-case-of-the-readable-dead-connection-a-ruby-mystery-smy8cygd3
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: The Case of the Readable Dead Connection: A Ruby Mystery | daily.dev
og:description: A detective-story-style postmortem explaining how a dead persistent HTTPS connection could pass a viability check and be handed to a new non-idempotent POST...
og:url: https://daily.dev/posts/the-case-of-the-readable-dead-connection-a-ruby-mystery-smy8cygd3
og:image: https://api.daily.dev/og/posts/smY8cYGD3.png
og:image:alt: The Case of the Readable Dead Connection: A Ruby Mystery
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# The Case of the Readable Dead Connection: A Ruby Mystery

**[RUBYLAND](https://daily.dev/sources/rubyla)** · 7 min read · 3 upvotes · 1 comments

## Summary

A detective-story-style postmortem explaining how a dead persistent HTTPS connection could pass a viability check and be handed to a new non-idempotent POST request, causing EOFError failures. The root cause: TCP-level socket readiness does not equal TLS-level application data availability — a close_notify alert makes the socket readable but yields no HTTP bytes. The fix introduced peek_partial, a layered non-blocking peek that lets TLS decode one pending record without consuming application data, so Async::HTTP v0.98.1 can detect and discard stale HTTP/1 connections before reuse.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://www.codeotaku.com/journal/2026-08/the-case-of-the-readable-dead-connection/index>

## Questions this post answers

### Why does my Ruby Async::HTTP client get an EOFError on a POST request after reusing a pooled connection?

The connection pool selected a connection whose peer had already sent a TLS close_notify shutdown alert, which makes the underlying socket appear readable even though no HTTP response bytes remain. Async::HTTP versions before v0.98.1 only checked socket-level readability, not whether TLS had decoded a shutdown record, so it handed a dead connection to a new non-idempotent request.

_Track fixes like this Async::HTTP connection-pool bug through daily.dev when debugging flaky HTTP retries._

### How does Async::HTTP's peek_partial method check if a pooled HTTP/1 connection is still alive?

IO::Stream::Readable#peek_partial makes one non-blocking read through the layered TLS stream before reuse: if no bytes are available it returns nil and leaves the stream untouched, if TLS decodes close_notify the stream is marked EOF and discarded, and if application data appears it stays buffered for the real reader. This lets Async::HTTP distinguish an idle-but-alive connection from one already shut down.

_Engineers building resilient HTTP clients can follow connection-pool internals like this on daily.dev._

### Is it safe to automatically retry a non-idempotent POST request after an HTTP connection failure?

No, retrying a non-idempotent POST after an ambiguous connection failure risks duplicating side effects like payments or record creation, because the client cannot know whether the server already processed the request. Async::HTTP addressed the underlying cause instead of relying on retries, by detecting dead pooled connections via TLS close_notify before assigning them to new requests, released in v0.98.1.

_Developers weighing retry safety for POST requests can find deep dives like this via daily.dev._

## Community discussion

Top comments from developers on daily.dev.

**@trevorsuna** · 0 upvotes

> Great example of why socket readiness is not application readiness. The layered peek is a neat fix because it lets TLS interpret the pending record without consuming the HTTP payload, so the pool can reject the stale connection safely.

## Similar posts on daily.dev

- [The occasional \`ECONNRESET\` \(part 1](https://daily.dev/posts/the-occasional-econnreset-part-1-hutyerwoz) · Hacker News · 0 upvotes · 0 comments
- [The Case of the Vanishing Fiber: A Ruby Mystery](https://daily.dev/posts/the-case-of-the-vanishing-fiber-a-ruby-mystery-dgqpxvnp4) · RUBYLAND · 1 upvotes · 0 comments
- [Inside Ruby’s net/http: Exploring the Networking Engine Behind Ruby APIs](https://daily.dev/posts/inside-ruby-s-net-http-exploring-the-networking-engine-behind-ruby-apis-hlfsozscx) · Ruby Flow · 0 upvotes · 0 comments
- [CVE-2026-46727: Use-after-free in pthread-based getaddrinfo timeout handler](https://daily.dev/posts/cve-2026-46727-use-after-free-in-pthread-based-getaddrinfo-timeout-handler-wsx4kaak6) · Ruby · 1 upvotes · 0 comments
- [Post Mortem: HTTP Request Smuggling Vulnerability](https://daily.dev/posts/post-mortem-http-request-smuggling-vulnerability-36fsdzezd) · Crystal · 7 upvotes · 0 comments

---

Tags: [#database](https://daily.dev/tags/database), [#ruby](https://daily.dev/tags/ruby)

[View this post on daily.dev](https://daily.dev/posts/the-case-of-the-readable-dead-connection-a-ruby-mystery-smy8cygd3)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"The Case of the Readable Dead Connection: A Ruby Mystery","url":"https://daily.dev/posts/the-case-of-the-readable-dead-connection-a-ruby-mystery-smy8cygd3","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/the-case-of-the-readable-dead-connection-a-ruby-mystery-smy8cygd3"},"datePublished":"2026-08-02T02:50:17.350Z","dateModified":"2026-09-14T08:43:32.589Z","description":"A detective-story-style postmortem explaining how a dead persistent HTTPS connection could pass a viability check and be handed to a new non-idempotent POST...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/378d4b821e1729002f54bd4a0b41db25?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/378d4b821e1729002f54bd4a0b41db25?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"RUBYLAND","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"RUBYLAND","logo":"https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/rubyla","url":"https://daily.dev/sources/rubyla"},"commentCount":1,"discussionUrl":"https://daily.dev/posts/the-case-of-the-readable-dead-connection-a-ruby-mystery-smy8cygd3","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":3},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":1}],"keywords":"database,ruby","timeRequired":"PT7M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"RUBYLAND","item":"https://daily.dev/sources/rubyla"},{"@type":"ListItem","position":3,"name":"The Case of the Readable Dead Connection: A Ruby Mystery"}]}
{"@context":"https://schema.org","@type":"WebPage","@id":"https://daily.dev/posts/the-case-of-the-readable-dead-connection-a-ruby-mystery-smy8cygd3","comment":[{"@type":"Comment","text":"Great example of why socket readiness is not application readiness. The layered peek is a neat fix because it lets TLS interpret the pending record without consuming the HTTP payload, so the pool can reject the stale connection safely.","datePublished":"2026-08-02T04:05:34.103Z","url":"https://daily.dev/posts/smY8cYGD3#c-sicyr68Yb","author":{"@type":"Person","name":"Trevor Suna","url":"https://daily.dev/trevorsuna","image":"https://media.daily.dev/image/upload/s--dZ7gXxpp--/f_auto/v1784081551/avatars/avatar_EMoP47rpuw8DNjhp6R1b6?_a=BAMAMicg0"}}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/the-case-of-the-readable-dead-connection-a-ruby-mystery-smy8cygd3#faq","mainEntity":[{"@type":"Question","name":"Why does my Ruby Async::HTTP client get an EOFError on a POST request after reusing a pooled connection?","acceptedAnswer":{"@type":"Answer","text":"The connection pool selected a connection whose peer had already sent a TLS close_notify shutdown alert, which makes the underlying socket appear readable even though no HTTP response bytes remain. Async::HTTP versions before v0.98.1 only checked socket-level readability, not whether TLS had decoded a shutdown record, so it handed a dead connection to a new non-idempotent request. Track fixes like this Async::HTTP connection-pool bug through daily.dev when debugging flaky HTTP retries."}},{"@type":"Question","name":"How does Async::HTTP's peek_partial method check if a pooled HTTP/1 connection is still alive?","acceptedAnswer":{"@type":"Answer","text":"IO::Stream::Readable#peek_partial makes one non-blocking read through the layered TLS stream before reuse: if no bytes are available it returns nil and leaves the stream untouched, if TLS decodes close_notify the stream is marked EOF and discarded, and if application data appears it stays buffered for the real reader. This lets Async::HTTP distinguish an idle-but-alive connection from one already shut down. Engineers building resilient HTTP clients can follow connection-pool internals like this on daily.dev."}},{"@type":"Question","name":"Is it safe to automatically retry a non-idempotent POST request after an HTTP connection failure?","acceptedAnswer":{"@type":"Answer","text":"No, retrying a non-idempotent POST after an ambiguous connection failure risks duplicating side effects like payments or record creation, because the client cannot know whether the server already processed the request. Async::HTTP addressed the underlying cause instead of relying on retries, by detecting dead pooled connections via TLS close_notify before assigning them to new requests, released in v0.98.1. Developers weighing retry safety for POST requests can find deep dives like this via daily.dev."}}]}
```

