diff --git a/raven/knowledge/_manager.py b/raven/knowledge/_manager.py index d3d8d701a..8536e6f8b 100644 --- a/raven/knowledge/_manager.py +++ b/raven/knowledge/_manager.py @@ -386,7 +386,7 @@ def document_path(self, document_id: str) -> Path | None: Beside ``read_document`` rather than instead of it: the indexer wants the bytes, and a viewer wants a handle it can stream and convert from. - Reading a 25 MB upload into memory to hand it back out again is the + Reading a 100 MB upload into memory to hand it back out again is the thing this exists to avoid. The path is inside raven's state directory, which the viewer's own path diff --git a/raven/rpc/files.py b/raven/rpc/files.py index d5b47cb20..44190fc7c 100644 --- a/raven/rpc/files.py +++ b/raven/rpc/files.py @@ -20,14 +20,24 @@ # does anything useful with the bytes, and the read would stall the event loop. MAX_VIEW_BYTES = 25 * 1024 * 1024 -# The largest file `fs.upload` accepts. It sits beside the viewer's ceiling -# because the two are one story told in both directions, and here rather than -# in the method that enforces it because the WebSocket transport has to size -# its own frame ceiling from it: an upload rides as base64 inside one JSON-RPC -# frame, so a transport ceiling below this one rejects the upload before the -# method can explain why, and the page sees a dropped socket instead of a -# reason. See `frame_ceiling_for_upload`. -MAX_UPLOAD_BYTES = 25 * 1024 * 1024 +# The largest file `fs.upload` accepts. Deliberately above the viewer's +# ceiling: the two answer different questions. An upload only has to land a +# path in the workspace -- a non-image attachment is named to the model, never +# read into the message -- while the viewer has to render what it serves, which +# is what keeps MAX_VIEW_BYTES where it is. A file between the two ceilings can +# therefore be attached and not previewed. +# +# It lives here rather than in the method that enforces it because the +# WebSocket transport has to size its own frame ceiling from it: an upload +# rides as base64 inside one JSON-RPC frame, so a transport ceiling below this +# one rejects the upload before the method can explain why, and the page sees a +# dropped socket instead of a reason. See `frame_ceiling_for_upload`. +# +# The ceiling is memory, not policy. One upload at this size costs the gateway +# the frame, the JSON string parsed out of it and the decoded bytes at once, +# and the parse holds the event loop while it runs. Raising this much further +# wants the streamed HTTP upload path instead, not a bigger number. +MAX_UPLOAD_BYTES = 100 * 1024 * 1024 # Room for the JSON-RPC envelope around a maximal upload: the method name, the # session key and the file name, plus escaping. Two orders of magnitude more diff --git a/tests/test_rpc_transport.py b/tests/test_rpc_transport.py index 519a831da..bc53f89ec 100644 --- a/tests/test_rpc_transport.py +++ b/tests/test_rpc_transport.py @@ -654,7 +654,7 @@ async def test_the_socket_carries_a_frame_larger_than_aiohttp_would_allow_by_def def test_the_frame_ceiling_can_carry_the_largest_upload_the_method_accepts() -> None: """The two limits are one limit, and this is what keeps them that way. - Sized arithmetically rather than by building a 25 MB payload: the envelope + Sized arithmetically rather than by building a maximal payload: the envelope is what has to fit around a maximal base64 body, and materialising one to learn its length would cost the suite a second to answer a question about two integers. diff --git a/ui-web/src/lib/upload.ts b/ui-web/src/lib/upload.ts index e26bd343c..557220bf7 100644 --- a/ui-web/src/lib/upload.ts +++ b/ui-web/src/lib/upload.ts @@ -14,7 +14,7 @@ import { t } from '../i18n/t' -const UPLOAD_MAX_BYTES = 25 * 1024 * 1024 +const UPLOAD_MAX_BYTES = 100 * 1024 * 1024 /* Two entries because the two callers hold the file in different forms, and asking for the wrong one costs the reader real time: a caller with the `File`