Repository navigation
Conversation
itscark
force-pushed
the
fix/binary-multipart-parts
branch
from
October 3, 2026 16:05
c53585b to
e0e952f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five defects, found while moving a Laravel Nova suite (≈370 browser tests) to 5.x. The first three come with a test that is red on
5.xand green with the fix, the last two are behaviour the existing suite covers; all 381 tests, pint, rector and phpstan stay green.1. Uploaded files arrive corrupted
parseMultipartBody()runs every part throughmb_ltrim($part, "\r\n").mb_ltrim()replaces each byte that is not valid UTF-8 with?, and a file part is binary:Any image upload therefore fails Laravel's
imagerule on the way in ("The logo field must be an image"), while a text file passes, which is why the existingUploadTestdid not notice. The leading CRLF is now stripped with a byte regex (pint'smb_str_functionsrule would rewrite a plainltrim()straight back). New test:it keeps the bytes of an uploaded file that are not valid UTF-8.2. A 204 or 304 is framed chunked, and Chromium reads the terminator as the next response
handleRequest()buildsnew Response($status, $headers, $content). Amp's constructor callssetBody()first, which setsContent-Lengthfrom the string, and thensetHeaders(), which replaces every header with the given ones — so the length is dropped again and amphp/http-server falls back toTransfer-Encoding: chunked. For a response without a body that means it writes the headers and then0\r\n\r\n. Chromium treats those five bytes as the start of the next response on the keep-alive socket; when they arrive after it has already reused the socket, that request fails withnet::ERR_INVALID_HTTP_RESPONSEand whatever it asked for never loads. Under CPU load (CI) this is a random blank page: in our suite the resource that lost was Nova's core bundle, withcreateNovaApp is not definedas the only trace, and a later click on the dead page hung the worker. The organic trigger is the 304 the browser gets when it revalidates a cached script or stylesheet, plus any 204 the application answers.The response is now built in the order the constructor should use: status, headers, then body, so every string body carries its
Content-Length. New testVisit/BodilessResponseTestreads the raw bytes off the socket for a 204, a 304 and a normal body.3. A late response of an earlier request is taken for the current one
With the action timeout reaching Playwright again (5.0), calls can now time out — and some of them time out after their consumer has stopped reading:
Client::execute()returns from agotoas soon as thewaitUntilstate arrives, andquerySelectorreturns on the handle's__create__event, both before the request's own response. Those responses were always left in the socket; while they were successes that nobody read, nothing happened. On a busy runner the late response is an error — "Timeout 1000ms exceeded" for a navigation that had long reachedload— and the client handed it to whatever request was in flight, because it threw on any error message andprocessResultResponse()returned the firstresult.valueit saw.Wire capture from a 367-test Laravel suite run next to ten CPU hogs (every request logged with its id):
And the second shape: the request that was wrongly failed never has its own answer read either, so the next call gets it —
Page::javaScriptErrors(): Return value must be of type array, true returnedwasassertNoJavaScriptErrors()receiving anisVisibleresult. 363 stale value responses and 22 stale errors in that one run.Client::execute()now passes over any message whoseidis another request's; events (noid) and the request's own response are handed on as before. Unit-tested with a scripted connection (tests/Unit/Playwright/ClientTest): a late error and a late result of an earlier request are ignored, the request's own error is thrown, events still come through. Both "late" tests are red on5.x.4. The attempt timeout no longer stays at one second whatever the configured timeout is
Execution::waitForExpectation()capped every attempt atusingTimeout(1_000, …). That cap dates from when the whole timeout was one second; the overall timeout became configurable later and the attempt did not follow, so on a runner where a single round trip takes longer than a second every attempt fails the same way until the budget is gone. An attempt is now a fifth of the configured timeout, at least one second — unchanged for the 5 s default and for this suite's 2 s, and a project that sets->timeout(15_000)for its CI gets 3 s attempts.5. Navigations are not retried
navigate,refresh,backandforwardjoinclick& co. as non-awaitable: a repeatedgotorestarts the page load the previous attempt was waiting for, so under load none of the attempts ever finishes (the capture above shows five one-second navigations in a row, each aborting the last). Playwright already waits for the load state itself, with the full timeout.Same
5.xsuite before/after: 381 passed, pint, rector and phpstan clean.🤖 Generated with Claude Code