Skip to content

Conversation

@epeicher
Copy link
Contributor

@epeicher epeicher commented Dec 15, 2025

Modified PHPRequestHandler to use php.runStream() instead of php.run() to avoid buffering large responses in memory. This prevents "Array buffer allocation failed" errors when downloading files larger than 2GB.

Updated all related components to support both PHPResponse and StreamedPHPResponse return types, ensuring backward compatibility for smaller responses while enabling efficient streaming for large file downloads.

Motivation for the change, related issues

STU-73
Related to Automattic/studio#1058

When downloading large files (>2GB) through WordPress Playground, the system attempted to buffer the entire response in memory, causing "Array buffer allocation failed" errors. This occurred because PHPRequestHandler used the deprecated php.run() method which buffers responses. By switching to php.runStream(), responses now stream directly to the client without requiring large memory allocations, enabling downloads of files up to several gigabytes in size.

Implementation details

The core change is in PHPRequestHandler.request(), which now calls php.runStream() instead of the deprecated php.run() method. This returns a StreamedPHPResponse containing ReadableStreams for headers, stdout, and stderr, rather than buffering the entire response in memory.

All methods in the request chain were updated to return Promise<PHPResponse | StreamedPHPResponse> to support both buffered and streamed responses. This maintains backward compatibility for small responses while enabling streaming for large files.

In start-server.ts, the HTTP handler now detects StreamedPHPResponse instances and streams the stdout directly to the HTTP response using a ReadableStream reader, avoiding memory allocation. Buffered responses continue to work as before.

Other components like export-wxr.ts were updated to handle both response types by checking instanceof StreamedPHPResponse and accessing stdoutBytes for streamed responses or bytes for buffered ones.

Testing Instructions (or ideally a Blueprint)

TBD

… files

Modified PHPRequestHandler to use php.runStream() instead of php.run() to avoid
buffering large responses in memory. This prevents "Array buffer allocation failed"
errors when downloading files larger than 2GB.

Updated all related components to support both PHPResponse and StreamedPHPResponse
return types, ensuring backward compatibility for smaller responses while enabling
efficient streaming for large file downloads.
@epeicher epeicher force-pushed the stu-73-site-larger-than-2gb-cant-be-exported-using-all-in-one-wp-v2 branch from 68eca49 to d5702cb Compare December 15, 2025 18:14
@adamziel
Copy link
Collaborator

adamziel commented Dec 16, 2025

@epeicher This seems like a pretty good idea! It seems like holding on to PHPResponse in the return type Promise<PHPResponse | StreamingPHPResponse> makes things more complex than they need to be with all the if/else checks. I wonder if going all the way to Promise<StreamingPHPResponse> would make things easier. Also, aliasing stdoutBytes as bytes and stderrText as errors on the StreamingPHPResponse type (with a relevant @deprecated comment in place since the very beginning) may bring it closer to PHPResponse and reduce the amount of refactoring we have to do to get this change off the ground.

@epeicher
Copy link
Contributor Author

@epeicher This seems like a pretty good idea! It seems like holding on to PHPResponse in the return type Promise<PHPResponse | StreamingPHPResponse> makes things more complex than they need to be with all the if/else checks. I wonder if going all the way to Promise<StreamingPHPResponse> would make things easier. Also, aliasing stdoutBytes as bytes and stderrText as errors on the StreamingPHPResponse type (with a relevant @deprecated comment in place since the very beginning) may bring it closer to PHPResponse and reduce the amount of refactoring we have to do to get this change off the ground.

Thanks for the input @adamziel! I was initially trying to minimize changes while I evaluated whether this approach was the right direction. Your suggestion makes sense though, and I agree it will simplify things. I will apply it and continue working on it, since the current approach works in Studio but not in Playground.

@epeicher
Copy link
Contributor Author

epeicher commented Dec 17, 2025

From the linked task:

Perhaps allinone migrations create a tmp directory somewhere like /tmp which is a MEMFS mount?

That's exactly right @adamziel, the ai1wm plugin creates a temporary archive locally, and that is the limitation I am currently facing while working on this.

The streaming approach suggested as part of this PR has worked for the playground CLI with Studio, but it totally breaks the Playground application. I would need to identify an approach that only uses streaming under very specific and controlled circumstances. Maybe using run() for normal requests and runStream() for file downloads?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants