Commits
Tarun Sukhani authored 6b9b283facf
[PF-32] Netty 4 Stage B: restore disk-spooling for large request bodies Stage A (PF-31) deleted the Netty 3-coupled StreamChunkAggregator and defaulted the pipeline to the stock HttpObjectAggregator with a 1 MB in-memory cap, which rejected large uploads with 413. This stage reintroduces a Netty 4-native streaming aggregator that keeps small bodies in memory and spools larger bodies to a temp file, so file uploads work end-to-end again without unbounded heap usage. New StreamChunkAggregator (ChannelInboundHandlerAdapter): - Accumulates HttpRequest + HttpContent into a CompositeByteBuf while bytesReceived <= play.netty.spoolThresholdBytes (default 1 MB). - On crossing the threshold, opens a temp file under Play.tmpDir, drains the in-memory buffer to disk, and writes subsequent HttpContent direct to the file. - On LastHttpContent, emits a DefaultFullHttpRequest downstream: in-memory bodies travel as the request's content ByteBuf; spooled bodies use Unpooled.EMPTY_BUFFER and stash the temp File on a channel AttributeKey<File> SPOOLED_BODY for PlayHandler.parseRequest to pick up. Transfer-Encoding header is stripped and Content-Length is set to the aggregated length on emit. - Hard cap via play.netty.maxContentLength (default -1 = unlimited): exceeded requests are rejected with 413 + Connection: close, drained to LastHttpContent, then the connection is closed. No further work reaches PlayHandler for a rejected request. - Refcount discipline: every HttpContent.release() in finally; retain() on slices added to the CompositeByteBuf so they survive chunk release. - channelInactive and exceptionCaught delete any in-flight temp file so a dropped connection mid-upload doesn't leak Play.tmpDir entries. PlayHandler.parseRequest: - Reads ctx.channel().attr(SPOOLED_BODY).getAndSet(null) atomically on the IO thread (safe under HTTP/1.1 keep-alive request serialization) and switches between FileInputStream and ByteArrayInputStream body paths. The spool File and the FileInputStream are stashed on the Http.Request.args map under __play.spoolFile / __play.spoolStream. - New cleanupSpooledBody helper (idempotent) closes the stream and deletes the file; falls back to deleteOnExit() if delete fails (e.g., a Windows file-still-open race). NettyInvocation.run() finally-clause now calls cleanupSpooledBody so disk leaks can't survive a controller throwing. Pipeline defaults flipped from HttpObjectAggregator back to play.server.StreamChunkAggregator in HttpServerPipelineFactory and SslHttpServerPipelineFactory. The aggregator defines its own no-arg constructor that reads play.netty.spoolThresholdBytes and play.netty.maxContentLength from Play.configuration, so the existing reflective handler-construction path needs no special-case for it. HttpObjectAggregator's int-arg fallback in getInstance() is preserved for users who explicitly wire it. application-skel/conf/application.conf documented defaults updated; adds a brief note on play.netty.spoolThresholdBytes vs maxContentLength. Verified with `ant clean unittest`: 295 tests pass, 0 failures. PF-33 (Stage C) will restore WebSocket support and retire the deprecated Http.Inbound(ChannelHandlerContext) ctor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>