GSoC 2026 Proposal Draft: HTTP Streaming #239
Piyush-Goenka
started this conversation in
Ideas
Replies: 2 comments 2 replies
-
|
This is an exceptionally strong and mature proposal as is. Several minor comments:
|
Beta Was this translation helpful? Give feedback.
0 replies
-
|
Hi @rsamoilov
Is there anything else that is required to be changed in the proposal apart from this ? |
Beta Was this translation helpful? Give feedback.
2 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment

Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Project Proposal
1. Project Title
HTTP Streaming Support for Rage Framework — Mentored by @rsamoilov @tonekk
2. Project Size
Medium (~175 hours)
This scope is appropriate because the work involves:
http_streamfrom scratch for chunked transfer encodingThis is not a small patch — it touches core networking internals in C and the Ruby-facing API layer. It is scoped to HTTP/1.1 only in this phase, keeping it within the 175-hour medium boundary.
3. Project Length
Medium project (~175 hours) over 12 weeks.
Proposed coding period: May 27 – August 25, 2026
No major scheduling conflicts during this period. I will be available full-time for the duration of the program. Any minor constraints will be communicated to mentors in advance.
4. Abstract
Modern streaming applications like ChatGPT, Claude, and Grok deliver content incrementally using HTTP Chunked Transfer Encoding. Rage, a fiber-based Ruby web framework built on the Iodine server, currently buffers all responses before sending — making real-time streaming impossible. This project implements core HTTP streaming support across two repositories: the Iodine C layer (transport, backpressure, chunked encoding) and the Rage Ruby layer (developer-friendly API, fiber integration). The result will be a declarative, protocol-agnostic streaming API —
render stream: enumerator— that unlocks real-time capabilities in Rage while keeping the interface abstract enough to support HTTP/2 in the future.5. Problem Statement
Scope
This project is explicitly scoped to:
✔️ HTTP Streaming via Chunked Transfer Encoding
✔️ HTTP/1.1 transport (HTTP/2 design-ready)
✔️ Developer-friendly protocol-agnostic API
✖️ NO Server-Sent Events (SSE) — out of scope
✖️ NO WebSocket changes — existing path untouched
✖️ NO HTTP/2 implementation — future work only
SSE support is a natural future extension of this work — the architecture is explicitly designed to support it with minimal additional effort.
What Exists Today
Iodine currently handles Rack responses in two ways:
String body — entire response sent in one shot:
each body — all chunks collected into a C buffer, then sent as one shot:
Codebase Evidence
iodine_http.c— Iodine accepts Enumerator bodies since Enumerator responds to#each, but the full#eachloop is collected into a C buffer before sending — not streamed progressively.iodine/README.md— explicitly states "Rack streaming is not supported".api.rb— Rage controller builds@__bodyas an array. No streaming path exists in Rage today.The Core Problem
Even when an app produces chunks incrementally via
.each, Iodine buffers everything first. The user receives nothing until the entire response is collected:This makes real-time streaming — word-by-word LLM output, live data feeds, large file downloads — impossible without a protocol upgrade.
Why Rage is the Right Framework to Fix This
Rage is built on fibers — every request runs in its own fiber. Pausing a fiber on backpressure costs nothing and releases the thread for Iodine to drain. This makes Rage architecturally perfect for streaming. The missing piece is the transport layer support in Iodine.
6. Proposed Solution
Two Repositories, Clear Boundary
This project spans two repositories with a clean separation of responsibilities:
Approach Selection — Option A vs Option B
Two approaches were considered for implementing HTTP streaming in Iodine:
Option A adds a new streaming path directly in the normal Rack HTTP response flow, detecting a
body.call(stream)body type and handling it incrementally. Option B reuses the existingrack.upgrademechanism — the same path used today for WebSocket upgrades.Option B was rejected because
rack.upgradechanges the semantics of the connection entirely — the app takes full ownership of the raw socket. Using it for HTTP streaming would require apps to use upgrade semantics for normal responses, break the standard Rack response contract, and make HTTP/2 support impossible without rewriting app-facing code. It mixes two fundamentally different concerns into one abstraction.Option A is the correct path because it preserves the standard Rack HTTP contract, leaves existing String and
eachbody paths completely unchanged, and keeps the transport layer extensible for HTTP/2 without any app-facing changes. Streaming becomes a first-class Rack body type — not a protocol upgrade.Key Design Decisions
HTTP streaming in normal Rack HTTP path, not
rack.upgrade— streaming is added as a new body type in the standard response flow. This preserves the Rack contract, leaves existing paths untouched, and keeps the design extensible for HTTP/2.body.call(stream)as the streaming contract — the new body type follows the Rack streaming spec directly. Iodine detects this body type and hands a RackStream writer to the app. This is the boundary between Iodine's transport layer and the Ruby application layer.Enumerator normalization in Rage, not Iodine — when the developer writes
render stream: enumerator, Rage wraps the enumerator into abody.call(stream)proc internally. Iodine only ever seescall(stream). This keeps C scope minimal and follows Rage's cooperative concurrency model — if Iodine iterated the enumerator directly in C, any blocking operation inside it would freeze the entire event loop.Explicit
:would_blockover implicit blocking —stream.writereturns:would_blockwhen the buffer is full instead of blocking internally. The producer callsFiber.yield, releasing the GVL so Iodine can drain. This is the only design compatible with an event-loop server.Dual callbacks on backpressure — when a fiber pauses on
:would_block, bothon_drainandon_closeare registered.on_drainhandles normal resume,on_closeensures the fiber always wakes on disconnect. Registering onlyon_drainwould cause fibers to sleep forever if the client disconnects while blocked.Backpressure Strategy
Two backpressure strategies were explored for handling fast producers.
The first approach has
stream.writereturn:would_blockexplicitly when the buffer exceeds the high watermark. The producer callsFiber.yield, releasing the GVL so Iodine can drain the buffer freely. Once pending bytes drop below the low watermark,on_drainfires,IodineCalleracquires the GVL, and the fiber resumes. The event loop is never blocked.The second approach has
writeblock internally until the buffer drains — simpler for the caller, but it holds the GVL inside the wait loop. Since Iodine needs the GVL to runon_drain, the buffer never drains, the loop never exits, and the server deadlocks. A single streaming request would freeze all connections.The first approach is the only design compatible with an event-loop server and is what this project implements.
Developer-Facing API — Rage Side
The public API is declarative and enumerator-based:
Developer never sees
:would_block,Fiber.yield, orstream.close. All backpressure and lifecycle management is hidden inside Rage.Enumerator Normalization — Where the Conversion Happens
Decision: Rage normalizes Enumerator →
body.call(stream)proc.This was the most carefully considered architectural decision of this proposal. Two options were explored:
Option 1 — Rage converts (chosen)
Rage wraps the Enumerator into a
body.call(stream)proc internally. Iodine only ever seescall(stream).Option 2 — Iodine C converts (rejected)
Iodine detects Enumerator in C and iterates natively via
rb_funcall.Why Option 2 is fundamentally wrong:
For an event-driven server, the highest priority is never blocking the event loop. If Iodine C calls
enumerator.nextand the Ruby code inside the enumerator performs a blocking operation — reading a file, querying a database, making an HTTP request — the C layer waits for Ruby to return, freezing the entire event loop and starving all other connections.Why Option 1 is the only correct approach:
Option 1 follows Rage's cooperative concurrency model correctly:
Fiber.yieldis the glue that allows Ruby code to pause and hand control back to IodinePerformance concern is negligible: Ruby handles millions of method calls per second. The overhead of the Ruby loop is minimal compared to the cost of network I/O.
Internal conversion in Rage:
New Rack Body Detection Branch — Iodine Side
A new
call(stream)branch is added to Iodine's body detection iniodine_http.c:Existing paths are completely untouched — backwards compatibility guaranteed.
RackStream Writer Internals
Every streaming connection is managed by a
stream_ctx_tstruct in C:Initialized at
call(stream)detection — the earliest point where all fields are available:uuid— client already connectedstate— set toIDLEfiber—Fiber.currentavailablehigh_watermark/low_watermark— pre-configured constantsblocked— set tofalsestream_ctx_tMemory Lifecyclestream_ctx_tcannot rely on Ruby GC alone because it owns C-side connection state and also holds Ruby references. The proposed ownership model is:stream_ctx_twhen Iodine enters the newbody.call(stream)branch iniodine_http.c, right before creating the RackStream writer object.Iodine::Connectionattachment system. That system currently manages upgraded WebSocket/SSE/raw connection objects, but this feature stays in normal HTTP response flow, so streaming needs its own teardown path.fiberreference during GC and release it only after terminal teardown so the producer fiber cannot be collected while blocked.CLOSEDafterstream.close/ final chunk / response finish, teardown detaches drain/close callbacks, clears Ruby references, and freesstream_ctx_texactly once.This means custom teardown logic will be required for HTTP streaming; Iodine's existing upgraded-connection attachment lifecycle is useful as a reference, but it does not automatically own or free
stream_ctx_tin the normal HTTP response path.stream.write Decision Flow
Every
stream.write(data)call executes these checks in strict O(1) order:Cheapest checks first. Most expensive operation (actual send) only happens after all cheap checks pass.
Three-Threshold Backpressure Policy
Hysteresis between HIGH and LOW prevents stuttering. Producer never resumes at HIGH — waits until LOW, giving breathing room before the next pause.
HARD_MAX policy — Fail Stream:
Nine backpressure rules:
pending >= HIGH→:would_blockimmediatelypending <= LOWwriteis O(1) — returns immediately alwaysFiber.yieldonlycloseis idempotent in all statesStream State Machine
Full transition table:
Code Snippets
1. rack_stream_write in C
2. on_drain + on_close resume bridge in C
3. Rage-side enumerator wrapping
4. http_stream — to be implemented from scratch
Existing Rage Primitives to Hook Into
Rage already has fiber infrastructure. This project hooks into existing primitives — not building from scratch:
Complete Data Flow
Target Files
Iodine — Modify:
iodine/ext/iodine/http_internal.h— stream state types, ctx struct declarationiodine/ext/iodine/http.h— transport lifecycle API declarationsiodine/ext/iodine/http.c— generic HTTP stream primitivesiodine/ext/iodine/http1.c— implementhttp_streamchunked encodingiodine/ext/iodine/iodine_http.c— addcall(stream)branch to body detectionIodine — Create:
iodine/ext/iodine/iodine_rack_stream.h— RackStream writer headeriodine/ext/iodine/iodine_rack_stream.c— full RackStream writer implementationiodine/spec/integration/response_streaming_spec.rb— integration testsiodine/spec/support/apps/response_streaming.ru— test Rack app fixtureIodine — Update:
iodine/README.md— streaming API documentationRage — Modify:
lib/rage/controller/api.rb— addrender stream:supportlib/rage/fiber.rb— hook drain/resume into existing Fiber.awaitlib/rage/fiber_scheduler.rb— integrate stream pause/unblocklib/rage/rack_adapter.rb— detect streaming response typeRage — Create:
lib/rage/streaming.rb— Rage::Streaming enumerator wrapperspec/stream_spec.rb— streaming integration testsRage — Update:
README.md— developer guide for streaming APIEdge Cases Covered (Behavior Matrix)
7. Project Deliverables
Iodine (C Layer)
http_streamfunction — core chunked transfer encoding, implemented from scratchstream_ctx_tstruct — RackStream writer context with all 6 fieldsrack_stream_write— O(1) write with all checks and return semanticsrack_stream_close— idempotent, safe in all stateson_draincallback — buffer drain resume with LOW_WATERMARK checkon_closecallback — disconnect safety, fiber never sleeps foreverRage (Ruby Layer)
render stream: enumerator— declarative developer APIRage::Streaming— enumerator → body.call(stream) conversionFiber.awaitandfiber_scheduler.rbBoth Repos
8. Timeline / Milestones
Community Bonding (May 1 – May 26)
iodine_http.c,http1.c,http.csourcefio_pending,fio_write,fio_is_valid, socket lifecycleIodineCallerandiodine_connection_fire_eventmechanismsfiber.rb,fiber_scheduler.rb,fiber_wrapper.rbiodine/lib/iodine/connection.rbon_drained callbackhttp_streamfunction signature with mentorsPhase 1 — Core C Layer (Week 1–3)
http_streaminhttp1.c— chunked transfer encodingbegin,write,finish) tohttp.chttp_internal.hstream state types and structsPhase 2 — RackStream Writer (Week 4–5)
stream_ctx_tstruct iniodine_rack_stream.hrack_stream_write— all 6 O(1) checks + return semanticsrack_stream_close— idempotent, safe in blocked staterack_stream_closed?on_stream_drain— LOW_WATERMARK check + IodineCaller resumeon_stream_close— disconnect safety, always wake fiberPhase 3 — Iodine Integration (Week 6–7)
iodine_http.cto detectcall(stream)body typebody.call(stream)crosses the C-to-Ruby bridge and sends the first chunk before full responsecompletion
Phase 4 — Rage API (Week 8–9)
Rage::Streamingenumerator wrapper inlib/rage/streaming.rbrender stream:support tolib/rage/controller/api.rbFiber.awaitinlib/rage/fiber.rbfiber_scheduler.rbpause/unblock mechanicslib/rage/rack_adapter.rbto detect streaming response:would_block, fiber yields,on_drainresumes, stream continuesPhase 5 — Backpressure & Edge Cases (Week 10–11)
Phase 6 — Testing & Documentation (Week 12)
iodine/README.mdwith streaming API guiderage/README.mdwithrender stream:developer guide9. Related Work
Existing Streaming in Iodine
Iodine supports persistent connections via
rack.upgradefor WebSocket. This project adds streaming to the normal HTTP response path without requiring protocol upgrades — a fundamentally different and complementary capability.Rails ActionController::Live
Rails implements streaming via
ActionController::Livewithresponse.stream.write. This project takes inspiration from that pattern but adapts it to Rage's fiber model — using a declarativerender stream: enumeratorAPI that is more idiomatic and requires no explicitensure stream.close.Node.js Streams
Node.js pioneered the
highWaterMark/ backpressure vocabulary. This project uses the same producer-consumer pattern with hysteresis (HIGH/LOW watermarks), adapted for Ruby's fiber model and Iodine's event loop.Rack Streaming Spec
The Rack spec defines
body.call(stream)as the interface for streaming bodies. This project implements full Rack streaming compliance in Iodine for the first time.HTTP/2 Compatibility
The API is designed to be protocol-agnostic. The
Rage::Streaminginterface abstracts the transport layer entirely — future HTTP/2 support requires only a new Iodine backend, not any app-facing API changes.SSE Extensibility
Although SSE is explicitly out of scope for this project, the architecture is designed so SSE becomes a natural and minimal extension.
SSE is HTTP streaming with a different
Content-Type(text/event-stream) and a specific chunk format (data: <payload>\n\n). Everything this project builds —stream_ctx_t, backpressure system, state machine,on_drain/on_closecallbacks, fiber scheduler integration — is completely reusable for SSE without any changes.Adding SSE in a future project would require only:
The transport contract, backpressure logic, state machine, and fiber integration stay completely untouched. This project builds the foundation — SSE is the natural next step.
10. Previous Contributions
iodine_http.c,http1.c,api.rb,fiber.rb, andUPGRADE_FLOW.mdto understand the existing architecture before proposing any changes11. About You
My name is Piyush Goenka. I am a second-year B.Sc. Computer Science student at BITS Pilani.
I was drawn to this project because it sits at the intersection of systems programming and real-world application — the same producer-consumer backpressure patterns that power LLM streaming, live data feeds, and incremental content delivery. Implementing this at the server level, in C and Ruby, is exactly the kind of deep systems work I want to do.
Open Source
I am an active open source contributor. I contributed extensively to the Palisadoes Foundation — the organization behind the Talawa project, a modular open source platform used by community-based organizations worldwide. My contributions were consistent and substantial enough that I was made a maintainer of the organization. This means I have real experience navigating large unfamiliar codebases, following contributor conventions, writing reviewable code, and taking responsibility for the quality of a shared project.
Achievements
I won Smart India Hackathon (SIH), India's largest national-level hackathon, which required building and shipping a working technical solution under a tight deadline — a skill directly relevant to a GSoC project with fixed weekly milestones.
I am comfortable contributing to codebases I did not write, communicating clearly with maintainers, and learning quickly from feedback. I am genuinely curious about low-level systems and enjoy going deep on problems rather than staying at the surface.
12. Availability
13. Communication Plan
14. Additional Notes
Why This Project Is Hard (And Why I Am Ready For It)
This project is rated Hard — not because the API shape is complex, but because of deep implementation challenges:
http_streamdoes not exist — must be implemented from scratch in Crb_fiber_resumerequires GVL acquisition viaIodineCaller, not a direct callon_drain+on_closecallbacks required so fiber never sleeps foreverenumerator.nextdirectly — Ruby blocking operations inside enumerators would freeze the entire event loopAll five of these challenges were identified and have concrete solutions documented in this proposal before submission.
Protocol Agnostic Design
Although the initial implementation targets HTTP/1.1, every design decision keeps HTTP/2 in mind:
Rage::Streamingabstracts the transport layerrender stream: enumerator) is transport-agnosticCooperative Concurrency Model
The three-layer model that makes this work:
This is why Rage's fiber architecture makes it uniquely suited for this feature — and why this project is the right time to implement it.
Beyond GSoC
GSoC is not the end goal for me — it is a starting point. I am genuinely interested in Rage as a project and in the broader problem of making Ruby web infrastructure faster and more capable. The mentorship and the deep dive into Iodine's internals during this project will leave me in a strong position to keep contributing meaningfully.
After the summer I intend to continue — whether that means working on follow-up features like SSE or HTTP/2 streaming, reviewing PRs, helping new contributors navigate the codebase, or tackling other open issues. I want to give back to the community that I will have learned from, not just complete a project and move on.
Beta Was this translation helpful? Give feedback.
All reactions