Commit 273ca06
authored
fix(files): serve rendered documents instead of source code (#6139)
* fix(files): serve rendered documents to attachments and workflow reads
Generated documents store their generation source under a .pdf/.docx name and
keep the compiled binary in a separate content-addressed artifact store, so any
consumer doing a raw read handed out source text under a document name.
- Route attachments and readUserFileContent through the servable resolver so
they get the compiled artifact, and stop the internal generation-source MIME
marker reaching providers as a content type.
- Render on read when the artifact is missing. The artifact key is (workspace,
source hash), so forking a workspace, moving a file, or editing the source
outside a recompiling writer orphaned it permanently and reported "still
being generated" forever. Rendering self-heals those and stores the result.
- Fall back to serving stored bytes as application/octet-stream when a render
fails, instead of failing forever, and remember not to retry those bytes.
- Only compile without a workspace context when the file's type positively
says it is generation source, so unrelated stored bytes are never executed.
- Make the doc-not-ready error opt-in per caller and give it a 409 via
HttpError, so output decoration degrades instead of failing completed work.
* fix(files): keep render failures retryable and never relabel unrendered bytes
Addresses the first review round.
- Only memoize a render failure when it is deterministic. A DocCompileUserError
means the source will never render, so remembering it is safe; sandbox
outages, timeouts, and cancellations are transient and were stranding valid
documents for the life of the process. Infra failures now propagate, which
also restores DocCompileUserError reaching callers that map it to 409.
- Refuse to hand back bytes the resolver could not render. readUserFileContent
returns a string, so the resolver's honest application/octet-stream could not
travel with it and attachment builders re-inferred a document MIME from the
filename — shipping generation source to a provider as a PDF. The file-serve
route keeps the graceful passthrough, where a human downloading the bytes is
useful.
- Normalize the declared type once so a padded or upper-cased source marker
cannot pass the resolver gate on one code path and fail it on the other.
- Import the doc-not-ready guard lazily. The static import pulled the
doc-compile module graph (remote sandbox, task runner, execution limits) into
every hydration consumer and broke an unrelated test's module mock in CI.
* perf(files): coalesce concurrent renders of the same missing artifact
An artifact miss is identical for every concurrent reader — a freshly forked
workspace whose document several viewers open at once, or one request whose
blocks read the same file — and each was paying for its own compile of the same
bytes. Share one in-flight render per (workspace, source, ext) key and drop the
entry as soon as it settles, so a later read still re-renders normally.
* fix(files): refuse unrendered bytes at the download boundary
Addresses the second review round.
- Throw UnrenderableDocumentError from downloadServableFileFromStorage instead
of returning bytes with an `unrendered` flag. Around 45 call sites (email
attachments, cloud uploads, zip entries, provider attachments) receive only a
Buffer and re-infer the type from the filename, so a flag they must remember
to check is a flag they will not check. Those callers already handled the
previous not-ready throw, so failing is the shape they expect. The file-serve
route is unaffected — it resolves bytes directly and keeps the graceful
passthrough, where a human downloading the file has a use for it.
- Surface that failure through hydration: with throwOnDocNotReady set, the
caller cannot use a file with no content, so an unrenderable document now
reaches it verbatim instead of degrading to null and reporting a misleading
"may exceed size limit or no longer accessible".
- Stop a shared render inheriting one caller's cancellation. The coalesced run
no longer carries any caller's signal; each caller races its own instead, so
an aborting reader gives up promptly while the render finishes for the others
and still lands in the cache.
* fix(files): move the unrenderable error out of the 'use server' module
file-utils.server.ts carries 'use server', whose exports must all be async
functions, so exporting an error class from it failed the production build with
67 cascading errors. The class now lives in the plain file-utils.ts beside the
other shared file helpers, which also lets the hydration path import it directly
instead of through a dynamic import.
Also bounds how long a failed render is remembered. The isolated-vm engine
cannot tell a bad source from a sandbox outage, so a permanent entry let one
transient failure block re-rendering that source for the life of the process.
Entries now expire after five minutes: long enough to stop a read loop spending
a sandbox run per read, short enough that an outage self-heals without a deploy.
* fix(files): finish the render cancellation and failure-surfacing edges
- Race the E2B render against the caller's signal too. Only the isolated-vm
branch did, so an aborted request on the E2B path waited for the sandbox to
finish and could return a success the caller no longer wanted.
- Attach a terminal handler to the shared render. Every caller races it against
its own signal, so all of them can walk away; a later rejection with no waiters
left would otherwise surface as an unhandled rejection.
- Stop narrowing what throwOnDocNotReady rethrows. readUserFileContent now runs
document compiles and can fail in ways this module has no business
enumerating; narrowing produced three consecutive review rounds of "this
particular failure is still swallowed". The flag means "do not degrade".
- Do not mark an unrendered response immutable. The serve route caches versioned
responses for a year, which would pin a one-off render failure to that URL long
after a later compile succeeds on the same version.
* revert(files): drop the concurrent-render coalescing
The coalescing was an optional efficiency win — rendering is content-addressed
and idempotent, so duplicate concurrent renders produced the same artifact and
cost only extra sandbox time on an artifact miss. It bought that at the price of
the most intricate code in the change set, and produced three concurrency
findings across two review rounds: a shared render inheriting one caller's
cancellation, an E2B/isolated-vm asymmetry in how the signal was raced, and
orphaned rejections once every caller could race away.
Removing it also restores true cancellation on the isolated-vm path: the caller's
signal now reaches runSandboxTask again, so an abort cancels the sandbox work
rather than only abandoning the wait for it.
* fix(review): simplify generated document attachments
* fix(files): mock servable downloads in hydration tests
* fix(files): preserve rendered attachment semantics
* fix(files): preserve cached artifact size
* fix(files): refuse unresolved xlsx source
* fix(files): resolve execution artifact workspace1 parent 06506bb commit 273ca06
10 files changed
Lines changed: 306 additions & 48 deletions
File tree
- apps/sim
- lib
- copilot/tools/server/files
- execution/payloads
- uploads/utils
- providers
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
541 | 541 | | |
542 | 542 | | |
543 | 543 | | |
544 | | - | |
545 | | - | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
546 | 547 | | |
547 | 548 | | |
548 | 549 | | |
| |||
Lines changed: 23 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
166 | | - | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
167 | 184 | | |
168 | | - | |
169 | 185 | | |
170 | 186 | | |
171 | 187 | | |
| |||
Lines changed: 56 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
Lines changed: 20 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
14 | | - | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
15 | 19 | | |
16 | 20 | | |
17 | 21 | | |
| |||
267 | 271 | | |
268 | 272 | | |
269 | 273 | | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
270 | 279 | | |
271 | 280 | | |
272 | 281 | | |
| |||
291 | 300 | | |
292 | 301 | | |
293 | 302 | | |
294 | | - | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
295 | 306 | | |
296 | 307 | | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
297 | 311 | | |
298 | 312 | | |
299 | 313 | | |
| |||
306 | 320 | | |
307 | 321 | | |
308 | 322 | | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
309 | 326 | | |
310 | 327 | | |
311 | 328 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
7 | | - | |
8 | | - | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
9 | 13 | | |
10 | 14 | | |
11 | 15 | | |
12 | 16 | | |
13 | 17 | | |
14 | 18 | | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
15 | 31 | | |
16 | 32 | | |
17 | 33 | | |
18 | 34 | | |
19 | 35 | | |
20 | | - | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
21 | 40 | | |
22 | 41 | | |
23 | 42 | | |
24 | 43 | | |
25 | 44 | | |
26 | 45 | | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
27 | 51 | | |
28 | 52 | | |
29 | 53 | | |
| |||
44 | 68 | | |
45 | 69 | | |
46 | 70 | | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
47 | 90 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
| |||
384 | 385 | | |
385 | 386 | | |
386 | 387 | | |
387 | | - | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
388 | 393 | | |
389 | 394 | | |
390 | 395 | | |
| |||
0 commit comments