Repository navigation
Replies: 1 comment 1 reply
|
Confirmed on The middleware
const middleware = compressionMiddleware({
level: config.compressionLevel,
threshold: config.compressionThresholdBytes,
filter(request, response) {
if (response.getHeader('content-range') !== undefined) return false
const contentType = response.getHeader('content-type')
if (typeof contentType === 'string' && contentType.toLowerCase().startsWith('text/event-stream')) return false
if (typeof contentType === 'string' && /^multipart\/form-data(?:;|$)/i.test(contentType)) return true
return compressionMiddleware.filter(request, response)
},
})
One supporting detail from the surrounding config: Also worth noting for the write-up
On the reference fix There are two shapes, and they differ in who has to change:
I would push for (1): it fixes the class without new API, and a per-response flush is the standard answer for "gzip + server-sent chunks over a non-SSE content type". A related symptom worth checking before you file Worth noting in the report which framing the response uses. If the route sets |
Uh oh!
There was an error while loading. Please reload this page.
Background
We are building Mobile Preview Plugin, an open-source DSH plugin for mobile developers. It shows and controls Android emulators and phones, iOS Simulators and USB-connected iPhones in the DSH side panel, so developers can watch and operate the app their agent is working on without leaving the conversation.
While qualifying the plugin on DSH Web and Desktop, we are running into host-side behavior that matters for plugins like ours. We will report these here one at a time, each with evidence and, where we can, a reference fix on our fork. This is the first one.
Where we hit this
The plugin streams the device screen as H.264 through a plugin fetch route (
application/octet-stream) and decodes it in the panel with WebCodecs. With a USB iPhone in Desktop, the preview drew about 2 frames per second and updated in visible two-second jumps, although the phone produced about 27 frames per second and the host wrote every packet within 90 ms. A mostly static screen was the worst case: the encoder then sends one large keyframe every two seconds and very little else, so the buffered output was released about once per keyframe.Summary
When the web server runs with
compression: gzip(the default from@deepseek-ai/dsh-web-app), a streamed/apiresponse is compressed but never flushed. The client receives it in large bursts instead of as it is produced.We observed it on DSH Web
0.2.1-alpha.1and Desktop0.2.0-rc.2; Desktop0.2.1-rc.2ships the same gzip configuration and bridge code. Desktop proxies/apito the same host web server.Cause
createGzipMiddleware(packages/host/webserver) exempts onlytext/event-stream. Its config documents that unknown-length streams are eligible for gzip, andmime-db@1.54.0marksapplication/octet-streamcompressible.packages/client/connection/src/http-bridge.ts) writes each response chunk withres.write(chunk)but never calls theres.flush()thatcompressionadds. zlib therefore keeps the output until its internal buffers fill or the response ends.Proposed fix
Flush after each chunk in the bridge, keeping compression and its streaming eligibility unchanged:
Commit on
dsh-v0.2.1-rc.2: mobile-dev-harness/deepseek-harness@82438f0 (compare with master)BridgeServerResponsegains an optionalflush(), documented as provided by a compressing middleware.res.flush?.()after eachres.write(chunk), before the existing backpressure wait.A new real-composition test in
packages/client/connection/tests/http-bridge.host.spec.tsstartsWebServerwithcompression: 'gzip'and has the handler produce its second chunk only after the client has decoded the first. Without the fix it fails after 5 seconds with "the first chunk was held by response compression"; with the fix it passes. The existingbinary-rpcgzip test still passes, including its check that the compressed transfer stays under a tenth of the payload.build:artifacts,build:checkand the commit hooks pass.doc-syncpasses exceptverify-repository-references, which already fails at687f282onpython/sdk/uv.lock:113and is unrelated to this change.Per-chunk
Z_SYNC_FLUSHcosts a few bytes per chunk, which is negligible for typical 16–64 KB chunks. If that matters for large bodies, coalescing flushes to one per event-loop turn is an alternative.Measurements
Measured in a plugin route, stock DSH Web with an iOS Simulator stream (the same pattern appeared on a USB iPhone in Desktop):
reader.read()Cache-Control: no-transformOn the host, the bridge pulled each chunk from the response stream within 1 ms of its enqueue in both cases, so the delay was entirely after the bridge's
res.write.Workaround for plugins
Cache-Control: no-transformon a streaming response disables compression for it; the Mobile Preview Plugin uses this on its media route.The change is MIT-licensed like DSH. Please feel free to take the commit as-is or reimplement it in whatever form fits.
cc @tianyicui @imccyu
All reactions