Skip to content

GC compaction in llhttp's MRI extension causes failed requests and process crashes #846

Description

@benubois

Hello,

TLDR: An update to llhttp is needed to fix an issue which causes requests from http to fail.


I recently updated to http ~> 6. After deploying, I started seeing a new exception:

TypeError: wrong argument type Hash (expected Class) parser << data ^^^^

[GEM_ROOT]/gems/http-6.0.4/lib/http/response/parser.rb#57 in HTTP::Response::Parser#add
[GEM_ROOT]/gems/http-6.0.4/lib/http/connection/internals.rb#134 in HTTP::Connection::Internals#read_more

This exception was reported about 1,000 times per day from a sidekiq process which makes around 900 http requests/second, but I don't think the issue is volume dependent. The attached reproduction triggers it with a single parser and one compaction and no concurrency.

The root cause is in llhttp's MRI C extension, not in http itself. It looks like in a long running process, garbage compaction is causing memory corruption in llhttp.

Two things make this worse than a normal exception:

  • Rescuing makes it worse. The memory stays stale after the first failure, so a process which rescues the error and keeps running eventually segfaults.
  • It may not always raise. An exception only occurs because the object in the slot doesn't respond to the callback method. An object that does respond would receive the response body and headers silently, with no error at all.

There's a fix available in bryanp/llhttp#40, but I thought since http is probably the largest consumer of this dependency, there are a lot of http users exposed to this issue.

I've attached a reproduction which illustrates the issue. Here are the instructions for how to get the patched version of llhttp to complete the demo:

mkdir demo

Download demo.txt to demo

git clone https://github.com/bryanp/llhttp
gh pr diff 40 --repo bryanp/llhttp | git -C llhttp apply --include='mri/*'
ruby -C llhttp/mri/ext/llhttp extconf.rb
make -C llhttp/mri/ext/llhttp
cp llhttp/mri/ext/llhttp/llhttp_ext.$(ruby -e 'print RbConfig::CONFIG["DLEXT"]') llhttp/mri/lib/

LLHTTP_LIB=$PWD/llhttp/mri/lib ruby demo.txt

Here's the output when I run it:

LLHTTP_LIB=$PWD/llhttp/mri/lib ruby demo.txt
ruby   ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [arm64-darwin25]
http   6.0.4
llhttp 0.6.2
  stock llhttp             /Users/ben/.rbenv/versions/4.0.6/lib/ruby/gems/4.0.0/gems/llhttp-0.6.2/lib/llhttp_ext.bundle
  patched llhttp (PR #40)  /Users/ben/Sites/http-error/llhttp/mri/lib/llhttp_ext.bundle

== parse: does a fresh parser survive one compaction?
   stock llhttp (10 processes)
     10x  ❌ CRASH [BUG] Segmentation fault at 0x…
   patched llhttp (PR #40) (10 processes)
     10x  ✅ OK parsed 10/10 after compaction

== misdispatch: what object do the callbacks actually reach?
   stock llhttp (10 processes)
      7x  ❌ NoMethodError: undefined method 'on_header_field' for #<Object:0x…> (decoys=20000)
      3x  ❌ CRASH [BUG] Segmentation fault at 0x…
   patched llhttp (PR #40) (10 processes)
     10x  ✅ OK parsed {"X-Account" => "12345", "Content-Length" => "12"}

== worker: does rescuing the error contain the damage?
   stock llhttp (10 processes)
     10x  ❌ CRASH [BUG] Segmentation fault at 0x…
   patched llhttp (PR #40) (10 processes)
     10x  ✅ OK survived 30 rounds

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions