Skip to content

feat: add unbuffered param to /drip to opt out of reverse-proxy response buffering#164

Closed
cyfung1031 wants to merge 2 commits into
jaredwray:mainfrom
cyfung1031:feat/drip-disable-buffering
Closed

feat: add unbuffered param to /drip to opt out of reverse-proxy response buffering#164
cyfung1031 wants to merge 2 commits into
jaredwray:mainfrom
cyfung1031:feat/drip-disable-buffering

Conversation

@cyfung1031

@cyfung1031 cyfung1031 commented Jul 18, 2026

Copy link
Copy Markdown

Please check if the PR fulfills these requirements

  • Followed the Contributing and Code of Conduct guidelines.
  • Tests for the changes have been added (for bug fixes/features) with 100% code coverage.

What kind of change does this PR introduce? Feature — opt-in unbuffered query param on /drip to ask a fronting reverse proxy not to buffer the streamed response.


Summary

/drip already does correct per-byte streaming at the application layer — reply.raw.write() one byte at a time with a real setTimeout between each write (src/routes/dynamic-data/drip.ts). But on the hosted mockhttp.org instance, which this repo's own README documents as running behind Cloudflare + Google Cloud Run, none of that per-byte timing survives to the client.

I confirmed this with three separate raw TLS socket captures (bypassing curl's own buffering to see exactly when bytes arrive on the wire):

GET /drip?duration=5&delay=1&numbytes=4096       -> entire response in one burst at t=6.32s
GET /stream-bytes/40960?chunk_size=1024          -> entire response in one burst at t=0.082s
GET /stream/20                                   -> entire response in one burst at t=0.079s

In every case, regardless of the requested duration/delay/chunk_size, the whole response arrives within tens of milliseconds — something in front of the app is buffering the complete response before releasing any of it. I ran into this migrating a project's onprogress test off httpbun, where it depended on genuinely incremental delivery to fire multiple progress events over a real download.

Fix

X-Accel-Buffering: no is a de facto standard response header (originally an nginx directive) that Cloudflare documents honoring to disable response buffering for a specific response. Since the app already streams correctly and the proxy layer is what's erasing it, this seemed like the right lever to expose rather than changing /drip's actual timing/content behavior.

Added it as an opt-in unbuffered=true|1 query param (default behavior unchanged), not always-on, for two reasons:

  • It has zero effect running directly against this server (only matters behind a buffering proxy) — no reason to always set it.
  • I have no way to verify from here whether Cloudflare actually honors it for mockhttp.org's specific Cloudflare + Cloud Run setup — only that the header is the documented, standard way to ask. I didn't want to claim this "fixes" streaming on the hosted instance when I can't confirm that end-to-end; opt-in lets you try it without committing to a behavior change if it turns out not to help.

Naming note: I initially called this no_buffering, but renamed it to unbuffered — a name with an embedded negation reads oddly if the off-state is ever set explicitly (no_buffering=false is a double negative), and unbuffered is standard, positively-framed terminology that doesn't require the caller to know a proxy is even involved.

I'm happy to help verify post-deploy with the same raw-socket capture methodology above, if that's useful once this lands on mockhttp.org.

Test plan

  • Added tests in test/routes/dynamic-data/drip.test.ts: header absent by default, present (no) for unbuffered=true and unbuffered=1, absent for an unrecognized value.
  • pnpm test (lint + full vitest run with coverage) passes locally: 477 tests, 100% line coverage, no lint warnings.
  • Manually verified against a local tsx src/index.ts instance: header present only when unbuffered=true is passed, absent otherwise.

…ponse buffering

/drip already streams bytes correctly at the application layer -
reply.raw.write() one byte at a time with a real setTimeout between
each write - but on the hosted mockhttp.org instance, which this
repo's own README documents as running behind Cloudflare + Google
Cloud Run, none of that per-byte timing survives to the client. I
confirmed this with three separate raw TLS socket captures against
/drip, /stream-bytes/{n}, and /stream/{n}: in every case the entire
response arrived in a single burst within tens of milliseconds,
regardless of the requested duration/delay/chunk_size, because
something in front of the app buffers the full response before
releasing it.

X-Accel-Buffering: no is a de facto standard response header
(originally an nginx directive) that Cloudflare documents honoring to
disable this kind of response buffering for a specific response. Since
the app already does correct real-time streaming and the proxy is
what's erasing it, this seemed like the right lever to expose, rather
than changing the endpoint's actual behavior.

This is opt-in via a new no_buffering=true|1 query param (default
unchanged) rather than always-on, for two reasons: it has zero effect
running directly against this server (only matters behind a buffering
proxy), and I have no way to verify from here whether Cloudflare
actually honors it for mockhttp.org's specific Cloudflare+Cloud Run
setup - only that the header is the documented, standard way to ask.
Happy to help verify post-deploy with the same raw-socket capture
methodology I used to diagnose this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a new no_buffering query parameter to the /drip route. When set to 'true' or '1', it appends the X-Accel-Buffering: no header to the response, instructing reverse proxies (such as Nginx or Cloudflare) to stream the response instead of buffering it. Unit tests have been added to cover the default behavior, the enabled state, and unrecognized values. There are no review comments, and I have no feedback to provide.

no_buffering embeds a negation in the name, which reads oddly if
anyone is ever explicit about the off-state (no_buffering=false is a
double negative). unbuffered is a standard, positively-framed term
("unbuffered I/O") that avoids that, and doesn't leak that a proxy is
involved - callers just want an unbuffered response, they don't need
to know why.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cyfung1031 cyfung1031 changed the title feat: add no_buffering param to /drip to opt out of reverse-proxy response buffering feat: add unbuffered param to /drip to opt out of reverse-proxy response buffering Jul 18, 2026
@cyfung1031

Copy link
Copy Markdown
Author

Sorry for using Vibe Coding.

In one of my projects (https://github.com/scriptscat/scriptcat/),
I am considering to migrate from https://httpbun.com/ to https://mockhttp.org/
However, I found that there are deviations regarding the drip API - httpbun can provide streaming responses but mockhttp always just give one response (buffered response).
I am worried that the changing of existing API behavior might break your tool, so I just suggest to add a new param "unbuffered" to enable the streaming behavior.

Screenshot 2026-07-18 at 9 11 14

Hope you might consider to merge this PR for the API enhancement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants