Repository navigation
[Bug]: Compressed asset responses lose their Content-Type, so HTML previews render as plain text #10935
Description
Activity
Triage
Confirmed. This is a real regression on
mainafter the Effect4.0.0-rc.112upgrade (#10652). Root cause is upstream: Effect-TS/effect#8146.httpCompressionLayerinapps/server/src/http.tsis Effect'sHttpMiddleware.compression(), applied globally. Workspace HTML/CSS/JS assets go throughassetFileResponse→HttpServerResponse.file. On Node, that putsContent-Typein headers only; the Raw body has no type. Compression rebuilds the body without one, andHttpServerResponse.setBodythen strips the header. The client getscontent-encoding: br,x-content-type-options: nosniff, CSP — and nocontent-type. Chromium therefore shows HTML as plain text and refuses CSS as a stylesheet.That matches the report: uncompressed requests keep
text/html; charset=utf-8; bodies under 1 KiB and non-compressible types (raster images) are skipped. Traces still showtext/htmlbecause they record headers before compression.Scope note: Effect's default compressible list includes
text/*,application/javascript, andimage/svg+xml, but notapplication/pdf. PDF previews should still send a type. Large SVGs can hit the same missing-type bug. Static app files are fine — they useHttpServerResponse.streamwithcontentTypeon the body.workspace-filegrants are directory-scoped, so sibling CSS/JS loaded by an HTML preview go through the same compressed file path.Next step
Apply the local workaround now: append
no-transformto the assetCache-Controlvalue inassetResponseHeaders(private, max-age=3600, no-transform; video overrideprivate, no-store, no-transform). The middleware honours that directive and leaves the type in place. Update the two exactCache-Controlexpectations inapps/server/src/http.test.ts.Track Effect#8146 as the real fix (carry
response.headers["content-type"] ?? body.contentTypeinto the compressed body). A local Effect patch is optional; the header change is enough to restore previews.Reacted by Stefano Faieta and Niklas Westman- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request acceptedvia-triageFiled through npx t3 triageFiled through npx t3 triage
on Sep 9, 2026 Another visible symptom of this: a custom project icon that points at an SVG larger than about 24 KB never shows. The client inlines icons up to 32 KB as data URLs, so a larger SVG loads from the signed asset URL instead. It arrives with
content-encoding: brand nocontent-type,nosniffmakes the<img>fail, and the project falls back to its automatic icon. PNG and ICO icons still work because the browser sniffs them.Reproduced on T3 Code Desktop 0.0.41-nightly.20260910 on Windows with a 56 KB and a 78 KB SVG. Setting the content type on the response body in
assetFileResponsemade the icon render in a desktop dev build, so theno-transformworkaround in #10948 should cover this too.Should have been solved by effect: Effect-TS/effect#8148
Reacted by Daniel StickerThis appears to be fixed on
main, though it's still in the latest stable release.- The upstream fix, Fix Content-Type preservation during HTTP compression Effect-TS/effect#8148 (merged Sep 9), ships in
@effect/platform-node@4.0.0-rc.115. Its compression now usesresponse.headers["content-type"] ?? body.contentTypefor Stream, Raw and Uint8Array bodies. - chore(deps): upgrade Effect to rc.115 and Alchemy to beta.78 #12326 upgraded T3 Code to Effect rc.115 on Sep 18 (d547e3b).
- v0.0.42, the current stable release, was cut on Sep 16 and still bundles rc.112, so it still has the bug. Opening a >1 KiB workspace
.htmlfile through the relay returnscontent-encoding: br,nosniffand nocontent-type, and the page shows as source. The same thing happens against the local server on 127.0.0.1 when the request includesAccept-Encoding.
Minimal repro without T3. It serves
HttpServerResponse.file(path, { headers: { "Content-Type": "text/html; charset=utf-8" } })throughHttpMiddleware.compression(), with exact-pinnedeffect,@effect/platform-nodeand@effect/platform-node-shared:rc.112 accept-encoding="gzip, deflate, br, zstd" -> content-type=undefined content-encoding=br rc.112 accept-encoding="" -> content-type=text/html; charset=utf-8 content-encoding=undefined rc.115 accept-encoding="gzip, deflate, br, zstd" -> content-type=text/html; charset=utf-8 content-encoding=br rc.115 accept-encoding="" -> content-type=text/html; charset=utf-8 content-encoding=undefinedSo this could probably be closed once v0.0.43 ships. The
no-transformworkaround PRs (#10948, #11768) look unnecessary now.- The upstream fix, Fix Content-Type preservation during HTTP compression Effect-TS/effect#8148 (merged Sep 9), ships in
Thanks for taking the time to report this and provide the details. We revisited it during the orchestrator V2 cleanup.
Pinned main uses Effect/platform-node rc.115, and the vendored compression implementation preserves response header content-type for Raw and Stream bodies. This matches the latest reporter comparison identifying rc.115 as fixed; the older stable release remains affected.
I’m closing this based on the current source and the evidence in this thread.
Resolved on audited main, not a claim that stable 0.0.42 contains the fix.
If you still hit this on a current build, please reply with the app/server versions and the steps that reproduce it. We can reopen this if the original problem is still there.
Before submitting
Area
apps/server
Steps to reproduce
mainat or after bd56e92 (the Effect rc.112 upgrade, chore(deps): upgrade Effect to rc.112 and Alchemy to beta.76 #10652). Reproduced against a dev server from 7220dfe; the relevant code is unchanged onmainas of 6c58362..htmlworkspace file larger than 1 KiB (assets.createUrlwith aworkspace-fileresource, which is what opening an HTML file in the file viewer does) and request it the way a browser does:Accept-Encoding.Expected behavior
Both responses carry
content-type: text/html; charset=utf-8and the sandbox CSP thatassetResponseHeaderssets, and the browser renders the page.Actual behavior
Step 2 answers
200withcontent-encoding: br,x-content-type-options: nosniffand the CSP header, and nocontent-typeat all. Step 3 answers withcontent-type: text/html; charset=utf-8. Because ofnosniff, Chromium shows the HTML source as plain text, and a.cssfile served the same way is refused as a stylesheet. Images are unaffected (the middleware does not compress them) and so are bodies under 1 KiB.The tracer records headers before the compression handler runs, so
http.server GETspans still showtext/htmlfor a response that reached the client without a type.Impact
Major degradation: every HTML, CSS, JS and PDF file the asset route serves to a browser that accepts compression, which is every browser.
Cause
httpCompressionLayerinapps/server/src/http.tsis Effect'sHttpMiddleware.compression(). Ineffect4.0.0-rc.112 the Node implementation rebuilds a file response around a body that carries no content type, andHttpServerResponse.setBodythen strips the header.HttpServerResponse.text(..., { contentType })survives because its body carries the type;HttpServerResponse.fileputs the type in the headers only. Details, a minimal reproduction and a suggested fix are in the Effect issue: Effect-TS/effect#8146Workaround
Append
no-transformto the assetCache-Controlvalue inassetResponseHeaders. The middleware honours the directive and asset responses go out uncompressed with their type. Two expectations inhttp.test.tspin the exact header value and need the same change.