Repository navigation
Sync Inertia updates and add DevTools support - #52
Conversation
inertiajs/inertia-laravel#891 adds Guzzle 8 support and requires at least Guzzle 7.15.2 on the 7.x line. Hypervel already allows Guzzle 8 framework-wide; this raises the 7.x floor to ^7.15.2 in the root manifest and every split package that requires Guzzle, so installs cannot resolve a release affected by GHSA-v5mv-p594-2x33 or GHSA-f7vp-7xgx-4w4r. The api-client and inertia manifests also declare hypervel/collections, which both packages use directly (Arr, Collection and collect()) but received only transitively. The inertia manifest also declares hypervel/filesystem for the DevTools entry repository. Upstream reference: inertiajs/inertia-laravel 3.x at 4da52b72da. Validation: composer validate for each split manifest, PackageMetadataTest and ComposerFileTest.
The concurrency, grpc and object-pool packages import Hypervel\Support\Arr or Hypervel\Support\Collection, which hypervel/collections provides, but their split manifests did not require it. They received the package only transitively. Each manifest now declares hypervel/collections directly. A scan of every split package found no other undeclared filesystem or collections imports; api-client and inertia gained the same requirement alongside their Guzzle floor change. Validation: composer validate for each manifest, PackageMetadataTest and ComposerFileTest.
Two upstream HttpGateway tests were missing or differed from the port: - inertiajs/inertia-laravel#817 added test_it_does_not_throw_exception_when_throw_on_error_is_disabled, which checks that a failed render returns null when throw_on_error is false. - inertiajs/inertia-laravel#885 asserts the head and body returned through a configured hot URL. Hypervel's equivalent now uses the upstream name, testItUsesConfiguredHotUrlWhenRunningHot, and the same response assertions alongside its URI check. The gateway source already matched upstream. Upstream reference: inertiajs/inertia-laravel 3.x at 4da52b72da. Validation: HttpGatewayTest and the Inertia suite.
inertiajs/inertia-laravel#848 added test_ssr_state_is_scoped_and_does_not_leak_between_requests for the request-scoped SsrState. Hypervel keeps that state in the coroutine-scoped InertiaState, and its equivalent test now uses the upstream name and dispatches through InertiaState::dispatchSsr(), as upstream's test does through SsrState, instead of setting the dispatch fields by hand. Upstream reference: inertiajs/inertia-laravel 3.x at 4da52b72da. Validation: ComponentTest and the Inertia suite.
Upstream declares SsrException::$event after fromEvent(). The port declared it first. Moving it restores upstream order so future merges line up; behavior is unchanged. Upstream reference: inertiajs/inertia-laravel 3.x at 4da52b72da.
Ports the server side of Inertia DevTools from inertiajs/inertia-laravel #892 and its follow-ups #894, #895, #896 and #897. While enabled, the adapter records each request (props and their Inertia types, shared-prop and render sources, route, headers and bodies) to local JSON entries and serves them to the browser extension from /_inertia/devtools/entries. Recording is limited to the local environment unless INERTIA_DEVTOOLS_ENABLED says otherwise, and the endpoints outside local require the configured gate. Hypervel adaptations: - The RequestHandled flush listener is registered only when DevTools is enabled at boot, so production requests pay nothing for it. It flushes before the response is sent, so the extension can fetch the entry as soon as the headers arrive. - EntryStore, SourceLocator, IncomingEntryBuilder and RequestRecorder are scoped per coroutine; the builder holds the request's source locator. - Source capture also skips Hypervel's own framework files, so path repository and monorepo installs report the application call site. - Upstream's Octane sandbox test is replaced by a coroutine isolation test covering concurrent requests in one worker. - EntryStore::flushState() resets the circuit breaker between tests. Upstream defects fixed: - Pruning ran in the listener, so a storage failure while pruning became a 500. It now runs inside EntryStore::flush(), behind the same failure breaker as the save. - A missing, empty or corrupt index was treated as empty, so the next save dropped every earlier entry from it. The index is now reseeded from the entry files under its lock, and recovery no longer overwrites an entry saved after the index was read. - Nested props are recorded under their dotted path, which bypassed key-based redaction, so a value such as auth.token was stored unredacted. A value is now redacted when any segment of its path is a sensitive key. - A partial devtools config section fell back to empty exclusion and redaction lists. Omitted lists now use the shipped defaults, owned by DevTools::DEFAULT_*; an explicit empty list still turns them off. - A numeric prop key reached a string-typed source lookup and returned a 500 under strict types. The frontend documentation gains a DevTools section adapted from inertiajs/docs v3/advanced/devtools.mdx at c6a69bd613. Upstream reference: inertiajs/inertia-laravel 3.x at 4da52b72da. Validation: every ported and added DevTools test file, the Inertia suite, PHPStan on the Inertia source and test subscriber, and php-cs-fixer.
…tion Every structured request walked its payload three times through recursive array_map closures: once to build the logical request data, once for the json option, and once more over that already-normalized logical data. On a 6 KB JSON page this added about 0.24 ms of client CPU per request over raw Guzzle, and about 1.5 ms on a large page. This showed up while measuring Inertia SSR requests sent through the HTTP client (inertiajs/inertia-laravel #916). The logical data built by parseRequestData() is no longer normalized a second time, and the remaining walks use a keyed foreach that builds a fresh array and skips the recursive call for scalar values. The added cost falls to about 0.16 ms on the 6 KB page and 0.6 ms on the large one. Key order, the Stringable, JsonSerializable and Arrayable handling and the transmitted JSON are unchanged. Upstream defect fixed: - The request header, multipart and fake response header normalizers assigned normalized values back into the caller's array, so a value passed by reference was changed in place: a Stringable header became a string, and a Stringable multipart part became a Guzzle stream once Guzzle built the body. They now build fresh arrays too. Caller data is left alone, and recorded multipart data no longer follows later assignments to a referenced variable. Laravel's PendingRequest and Factory have the same in-place assignments. Upstream reference: laravel/framework master at 588c1c948c. Validation: HttpClientTest, including regression tests for referenced JSON data, request headers, multipart contents and part headers, and fake response headers; the HTTP and API client suites; PHPStan on the HTTP source; php-cs-fixer; and before/after microbenchmarks with a concurrent load comparison.
Ports inertiajs/inertia-laravel #902. A JsonSerializable prop was passed through as is, so closures and Inertia prop types in the data it serializes to were never resolved. PropsResolver::resolveValue() now unwraps JsonSerializable values after Responsable ones, so the resolver descends into the serialized data. Upstream reference: inertiajs/inertia-laravel 3.x at 4da52b72da. Validation: both upstream tests ported to PropsResolverTest, and the Inertia suite.
Ports inertiajs/inertia-laravel #908. AssertableInertia::loadDeferredProps() used is_callable() to tell a callback from a group name, so a group named after a global function, such as "auth", was taken for the callback and the assertion failed with a TypeError. It now checks for a Closure, which the method signature already requires for callbacks. Upstream reference: inertiajs/inertia-laravel 3.x at 4da52b72da. Validation: the upstream test ported to AssertableInertiaTest, and the Inertia suite.
Ports inertiajs/inertia-laravel #911. The @inertia directive and the <x-inertia::app> component embed the page object in a script tag. A prop containing "</script>" or "<!--" could close the tag early or change how the browser parses the rest of the page. Both now encode the page with JSON_HEX_TAG, keeping Hypervel's JSON_THROW_ON_ERROR. These are the only places the page JSON is embedded. Upstream reference: inertiajs/inertia-laravel 3.x at 4da52b72da. Validation: both upstream tests ported to DirectiveTest and ComponentTest, and the Inertia suite.
get(), head(), query(), post(), patch(), put() and delete() documented only ConnectionException. They also throw RequestException when the request is configured with throw(), throwIf() or a retry() that runs out of attempts. Static analysis therefore reported a correct catch of RequestException around these calls as unreachable. Laravel has the same gap. Validation: PHPStan on the HTTP client and the HTTP suite.
Ports inertiajs/inertia-laravel #916, together with #906 and #910, which change the same service provider, response factory and facade. #916: Inertia::configureSsrRequestUsing() registers a callback that receives the PendingRequest for each SSR render, health check and shutdown request, for example to add headers, timeouts or retries. SSR requests now go through Hypervel's HTTP client instead of a dedicated Guzzle client, on an inertia-ssr connection that the service provider registers at boot with the configured timeouts. The connection's shared transport handler keeps connections to the SSR server open between requests. Http::fake() and Http::preventStrayRequests() now apply to SSR, so the testing-only HttpGateway::useTestingClient() is removed. Hypervel adaptations: - A callback set during boot applies to every request. One set while handling a request is kept in that request's Inertia state, so concurrent requests do not share it. - SSR requests keep their 2-second connect and 5-second total timeouts. Setting either to null uses the HTTP client's global timeout, as Laravel's adapter does by default. - A configured throw() or retry() raises RequestException. The gateway uses the exception's response, so the SSR server's structured error still reaches SsrRenderFailed and does not start the transport backoff. Only ConnectionException counts as a transport failure. - SSR bodies are decoded with json_decode() rather than Response::json(), so the HTTP client's global JSON decoding flags cannot turn a malformed body into an exception instead of a client-side rendering fallback. Upstream's gateway throws in that case. - inertia:stop-ssr catches the HTTP client's ConnectionException, and the package no longer requires guzzlehttp/guzzle directly. #906: the Blade component namespace is registered on the compiler passed to the resolving callback. The Blade facade could resolve a different compiler than the one being built. #910: Inertia::back() declares Hypervel\Http\RedirectResponse, which Redirect::back() returns, instead of Symfony's base class, so helpers such as with() type-check on its result. Its $fallback parameter is narrowed from mixed to bool|string, matching Redirector::back(). The SSR section of the Vite documentation now covers configuring the request, adapted from inertiajs/docs v3/advanced/server-side-rendering.mdx at cf513d8ffc. docs/todo.md records benchmarking a Swoole coroutine transport for the SSR connection once the HTTP client supports one. Upstream reference: inertiajs/inertia-laravel 3.x at 4da52b72da. Validation: the ported upstream tests; HttpGatewayTest and StopSsrTest rewritten on Http::fake(); coroutine isolation, timeout, retry and JSON decoding regression tests; the Inertia, HTTP and Saloon suites; PHPStan; FacadeDocblocksTest; php-cs-fixer.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Summary by QodoSync Inertia 3.x, add DevTools, and modernize SSR requests
AI Description
Diagram
High-Level Assessment
Files changed (90)
|
|
Code Review by Qodo
1.
|
Corrects three gaps in how stored DevTools entries are redacted, all inherited from inertiajs/inertia-laravel's recorder: - Query strings were parsed with Uri::of() and rebuilt, which rewrote parameters that were not sensitive: q=a+b became q=a%2Bb, filter.name=x became filter%5Bname%5D=x and tags[]=1 became tags%5B0%5D=1. A malformed host made Uri::of() throw, so such URLs were stored without redaction. Redaction now works on the raw query pairs. A parameter is redacted when its decoded name, or any of its bracketed segments such as filter[secret], is a sensitive key; every other byte of the URL stays as recorded, and relative and malformed URLs are redacted the same way. - The Location, Referer and X-Inertia-Location headers carry URLs, but only body and request URLs were redacted. Their sensitive query parameters are now redacted too. - The value passes ran over the whole entry, including the props map, which is keyed by prop name and holds metadata rather than values. A prop named after a sensitive key, such as token, lost its metadata, and a prop named requestHeaders or responseHeaders was flattened as a header bag. The props map now stays out of the value passes (prop values are still redacted under propValues), and headers are normalized only in the entry's real request and response header bags. The DevTools documentation now says which data is redacted, and that other request and response bodies, such as HTML or plain text, are stored as sent, so paths whose responses contain secrets can be excluded. Validation: redaction regression tests for raw query pairs, bracketed and relative URLs, URL headers and prop metadata, each confirmed to fail without its fix; the Inertia suite; PHPStan; php-cs-fixer.
Corrects three storage gaps inherited from inertiajs/inertia-laravel's recorder: - When the _meta.json index could not be opened or locked, the update returned silently after the entry file was written. The entry never appeared in the index, so the extension could not list it and pruning never reached its file. The repository now throws, so the entry store logs the failure and starts its backoff like any other storage failure. The index rebuild's fallback to scanning the entry files is removed, since the update can no longer skip its callback. - The per-tab entry limit applied only to entries with a tab ID. Entries recorded without one, such as initial page loads and requests made without the extension, were bounded only by age. They are now limited as one group. - The breaker's backoff was set after the failure was logged. When the log shared the failing storage, such as a full disk, the logger's exception escaped the request observer and the backoff never started, so every request retried and failed again. The backoff is now set first, and a failure to log is ignored, since recording must never break the response. Two test corrections: the skipped-prune test now saves an expired entry, so it fails if the prune runs (a fresh entry survived either way), and a comment that claimed a misconfigured except list drops the entry now matches what its test asserts: the response still succeeds. Validation: regression tests for an unopenable index, the tabless limit and a failing logger, each confirmed to fail without its fix; the Inertia suite; PHPStan; php-cs-fixer.
Corrects how DevTools marks shared props and records where they were shared: - Share sources were held on the per-coroutine RequestRecorder, while the shared props themselves live in InertiaState, which carries props shared during boot into each request. A request's recorder started empty, so props shared from a service provider lost their source location. The sources now live in InertiaState beside the props, so they follow the same boot-to-request path and stay isolated between concurrent requests. - Inertia::flushShared() cleared the shared props but not their sources, so a later prop with the same name was shown with the old share location. Both are now cleared. - Shared keys were taken from the shared props before shared property providers were expanded, so props supplied by a ProvidesInertiaProperties provider were not marked as shared. The recorder now receives the shared props after expansion. This also applies when the page object does not expose shared prop keys. The last two are inherited from inertiajs/inertia-laravel. The redaction test also asserts that a prop named after a sensitive key keeps its shared flag and render source. Validation: coroutine isolation tests for boot-time and per-request share sources, with the test case's copying of non-coroutine context turned off so it matches a server request; provider and flushShared() regression tests, each confirmed to fail without its fix; the Inertia suite; PHPStan; php-cs-fixer.
DevTools classified any mergeable prop with matchOn() keys as a deep
merge, for example Inertia::defer(...)->matchOn('id') without merge().
The page only sends match keys for props that merge, so such a prop
replaces its value on the client, and the panel showed the wrong merge
behavior. A prop is now a deep merge only when it merges. Inherited from
inertiajs/inertia-laravel's classifier.
Validation: a classifier regression test, confirmed to fail without the
fix; the Inertia suite; PHPStan; php-cs-fixer.
Requests that only read the session, such as polling endpoints, still
saved it when they finished. Session data is saved as a whole, so a
polling request that started before a concurrent request saved new data
overwrote that data with its own older copy. It also aged flash data a
redirect was about to read and recorded the poll as the previous URL.
A route can now read the session without saving it:
Route::get('/notifications/unread', ...)->readOnlySession();
$request->session()->markAsReadOnly() does the same for the current
request. A read-only session still starts, so the request can read it
and authenticate the user, but it is never saved:
- Store::save() returns without writing, and regenerating or
invalidating the session does not destroy the stored session.
- StartSession skips garbage collection, the previous URL and the
session cookie, since the session's ID is never saved and the browser
keeps its current cookie.
- PreventRequestForgery does not add the XSRF-TOKEN cookie, since the
session's token is never saved either. Without this, a request that
regenerated the token, such as a remember-me login, would hand the
browser a token the next request rejects.
The flag is coroutine-local, cleared when the store is constructed and
when the session starts, so it applies only to the request that sets
it. Route caching keeps the option. The Session contract, facade
docblocks and session documentation are updated.
Validation: store tests for saving, regeneration, the reset on start
and coroutine isolation; middleware integration tests covering
persistence, garbage collection, exceptions thrown from the route and
cookies, including a session marked read-only during the request; a
remember-me login on a read-only route; compiled route caching; each
new test confirmed to fail without its change. The session, auth,
routing, HTTP, Inertia, Sanctum and Socialite suites, the full parallel
suite, PHPStan, php-cs-fixer and the facade docblock test pass.
Each test request runs in its own coroutine and copies its session, authentication and request state back to the test when it finishes, so the next request continues from it. Two paths copied the wrong state: - A read-only request copied back session changes and a regenerated ID that were never saved. The next request then read and saved them, so a test could pass while the application discards that data. The test now keeps the session it had before a read-only request, as the next real request would load the unchanged stored session. Authentication still syncs, as it does for other requests. - Redirects were followed inside the first request's coroutine, after it had already copied its state back. Each followed request copied its state to that coroutine, which then ended, so the test never saw it. After following a redirect to a page that read flash data, the next request saw the flash data again, and session data written while following redirects was lost. Redirects are now followed from the test coroutine, so request() afterwards is the final request, as in Laravel, and each followed request gets its own wait timeout. Validation: regression tests for both paths, each confirmed to fail without its fix; the full parallel suite; PHPStan; php-cs-fixer.
The DevTools extension fetches entries while the application's own requests are in flight, for example the moment a failed form POST responds and before the browser follows its redirect. The entry routes run the web middleware so the gate can authorize the user, which also saved the session when they finished. That save overwrote session data a concurrent request had saved after the entry request loaded it. Upstream (inertiajs/inertia-laravel) covers two symptoms with route middleware: PreserveFlashData stops the entry request from aging flash data, and PreventPreviousUrlTracking stops it from recording the entry URL as the previous URL. Neither stops the overwrite. The entry routes now use read-only sessions, which cover all three, and both middleware classes are removed. Validation: a regression test where a concurrent request saves newer session data during an entry request, confirmed to fail without the change; the existing flash data tests; the Inertia suite; PHPStan; php-cs-fixer.
The once-shared middleware test checked only that a recorded share source did not start with the framework directory, so it also passed when no source was recorded at all. Shares made inside Inertia's middleware have only framework pipeline and middleware frames above them, so the source locator finds no application frame and records nothing. Assert that the entry has no share source, which fails when the locator stops skipping framework frames.
The initial Inertia page gets a script tag carrying the DevTools entry id, so a panel that attaches after the page loads can find the entry. The injection looked only for a lowercase </body>, so a root view closing its body as </BODY> or </Body>, which is valid HTML, never received the tag. Find the last closing body tag case-insensitively and insert the script before it, leaving the page's own tag unchanged. A test renders a root view with uppercase tags and checks the script lands before </BODY>.
A save wrote the entry file first and then opened and locked the index to record its metadata. When the index could not be opened or locked, the save failed after the file was already written. The entry store retries after its short suppression window, so a lasting index problem left one more file on every retry, and the index never listed or pruned any of them. Write the entry file inside the index update, after the lock is held, so an open or lock failure throws before any file exists. The file and its index metadata are now written together under the exclusive lock, so pruning and tab limits, which read the index under a shared lock, see both or neither. The single-use index metadata helper is removed, and the failed-save test now also checks that no entry file is left.
Two problems made a stored DevTools entry disagree with the response it records. The rendered page was recorded before its response was built, and the recording stayed even when that page never reached the client. A root view that failed to render, or a page that failed JSON encoding, left the discarded page's component, props and body on the resulting 500 entry. On a version mismatch, the middleware replaces an Inertia request's page with a 409, and the entry still described the page. The page is now recorded only after its response is built, and an Inertia request's page data is dropped when the response it gets no longer carries the Inertia header. Error pages rendered with Inertia are still recorded as pages, whatever their status. The final storage pass redacted configured keys throughout the entry, including its own structure. Adding a common key such as id to the redaction list replaced the entry's id, so the listing advertised an id that could not be opened. Keys such as name or value broke the route name or replaced a whole captured body. Keys are now redacted only in application values: prop values, the captured body values, header bags and any other section. The entry's metadata, route and source details, prop metadata and body status are left as recorded. Sensitive query parameters in the entry's URLs are still redacted. Tests cover the version-change and missing-root-view responses, and a stored entry with id, name and value configured that is listed and then retrieved by its id.
The test that a prune is skipped until its interval elapses read the interval from config, which comes from INERTIA_DEVTOOLS_PRUNE_INTERVAL_SECONDS. With that variable set to 0, every prune is due, so the test failed on an environment setting rather than a code change. The test now sets the interval itself.
The DevTools gate decides who may view recorded entries, but the recorder records requests from every visitor while it is enabled. The frontend guide now says so, and advises enabling the recorder outside the local environment only where untrusted visitors can't reach the application.
The previous change kept the entry's metadata out of key redaction so that a configured key such as id could not replace the entry's own id. That also stopped a configured url or redirectLocation key from redacting the entry's URLs, so only their sensitive query parameters were redacted. A secret in the path, such as a password reset token, was stored as recorded. The entry's URLs are request data, so a URL stored under a configured key is now redacted whole again, as before that change. Other URLs keep query-parameter redaction, and the rest of the entry's structure is still left as recorded. A test covers an entry with url configured as a key.
The DevTools recorder adds a script tag to the initial HTML page, which lengthens the body. A Content-Length the application set for the page as rendered was still sent, and Swoole honors it, so the page reached the browser cut short. Response preparation only removes the header when Transfer-Encoding is set. Upstream has the same gap. The header is now removed once the tag has been added. Responses that don't get the tag keep their headers as they were. The tag injection test now starts from a page with a correct length and checks that the injected page no longer advertises it.
This brings Hypervel's Inertia adapter up to date with inertiajs/inertia-laravel
3.xat4da52b72da, apart from five recent changes (pull requests 915, 917, 888, 904 and 918) that will follow separately. The main additions are Inertia DevTools support and SSR requests sent through Hypervel's HTTP client. Measuring that SSR change led to a faster request data normalizer in the HTTP client, which also stops it from changing the caller's arrays. DevTools' entry requests also led to read-only sessions: a route option for requests that read the session without saving it, so they can't overwrite data saved by concurrent requests.Upstream Updates
The numbers below are inertiajs/inertia-laravel pull requests.
/_inertia/devtools/entries. Recording is limited to the local environment unlessINERTIA_DEVTOOLS_ENABLEDsays otherwise, and outside local the endpoints require the configured gate. The recorder is held per coroutine, so concurrent requests in one worker get separate entries, and the flush listener is only registered when DevTools is enabled at boot, so production requests don't pay for it. Source locations skip Hypervel's own framework files, so path repository and monorepo installs report the application's call site. Upstream's Octane sandbox test is replaced by a coroutine isolation test. The frontend documentation gains a DevTools section adapted from inertiajs/docsv3/advanced/devtools.mdxatc6a69bd613.Inertia::configureSsrRequestUsing(), which receives thePendingRequestfor each SSR render, health check and shutdown request, so you can add headers, timeouts or retries. SSR requests now go through Hypervel's HTTP client instead of a dedicated Guzzle client, on aninertia-ssrconnection registered at boot with the configured timeouts. The connection's shared handler keeps connections to the SSR server open between requests.Http::fake()andHttp::preventStrayRequests()now apply to SSR, so the testing-onlyHttpGateway::useTestingClient()is removed, and the package no longer requires Guzzle directly. The HTTP client costs a little more client CPU per render than raw Guzzle; with the normalizer change below, that's about 0.16 ms for a 6 KB page.configureSsrRequestUsing()during boot applies to every request. One set while handling a request is kept with that request, so concurrent requests don't share it. SSR keeps its 2-second connect and 5-second total timeouts, and setting either tonulluses the HTTP client's global timeout, as Laravel's adapter does by default. A configuredthrow()orretry()doesn't hide the SSR server's error: its structured response still reachesSsrRenderFailed, rather than being treated as a connection failure that starts the backoff. The Vite documentation covers configuring the request, adapted from inertiajs/docsv3/advanced/server-side-rendering.mdxatcf513d8ffc, and the README's differences now describe the timeouts.docs/todo.mdrecords benchmarking Swoole's coroutine HTTP client for this connection once the HTTP client supports it as a transport.Bladefacade could resolve a different compiler from the one being built.Hypervel\Http\RedirectResponseasInertia::back()'s return type, which is whatRedirect::back()returns, instead of Symfony's base class, so helpers such aswith()type-check on the result. Its$fallbackparameter is narrowed frommixedtobool|string, matchingRedirector::back(), which rejects anything else.JsonSerializableprop. They were passed through untouched.loadDeferredProps()in tests when a deferred group is named after a global function, such asauth. The group was taken for the callback and the assertion failed with aTypeError.@inertiadirective and the<x-inertia::app>component withJSON_HEX_TAG, so a prop containing</script>or<!--can't close the script tag early.^7.15.2, and Hypervel does the same in every package that requires Guzzle, so installs can't resolve a release affected by GHSA-v5mv-p594-2x33 or GHSA-f7vp-7xgx-4w4r.nullwhenthrow_on_erroris disabled (817), and a configured hot URL returns the rendered head and body (885). The gateway already matched upstream.InertiaState::dispatchSsr(), as upstream's does throughSsrState.SsrExceptionalso declares its members in upstream's order.Additional Hypervel Fixes
auth.tokenwas stored unredacted. A value is now redacted when any part of its path is a sensitive key. A partialdevtoolsconfig section fell back to empty exclusion and redaction lists; omitted lists now use the shipped defaults, while an explicit empty list still turns them off. Upstream has the same bugs.json_decode()rather thanResponse::json(). The HTTP client's global JSON decoding flags could otherwise turn a malformed body into an exception instead of a fallback to client-side rendering. Upstream's gateway throws in that case.Stringable,JsonSerializableandArrayablevalues are unchanged.Stringableheader became a string, and aStringablemultipart part became a Guzzle stream. They now build new arrays, and recorded multipart data no longer follows later changes to a referenced variable. Laravel has the same behavior.get(),head(),query(),post(),patch(),put()anddelete()documented onlyConnectionException, so static analysis reported a correctRequestExceptioncatch around them as unreachable. They now also documentRequestException, which they throw withthrow(),throwIf()or aretry()that runs out of attempts. Laravel has the same gap.hypervel/collections, which they use directly but only received through other packages. Inertia also requireshypervel/filesystemfor DevTools.->readOnlySession(), and$request->session()->markAsReadOnly()does the same for the current request. A request that only reads the session, such as a polling endpoint, otherwise saves its whole copy when it finishes and can overwrite data a concurrent request saved in the meantime. A read-only session still starts, so the request can read it and authenticate the user, but it's never saved, regenerating it doesn't destroy the stored session, and no session orXSRF-TOKENcookie is sent. Route caching keeps the option, and the session documentation covers it.PreserveFlashDataandPreventPreviousUrlTrackingmiddleware, which covered only the flash data and the previous URL, are removed.Uri, which rewrote parameters it didn't redact (q=a+bbecameq=a%2Bb, andfilter.name=xbecamefilter%5Bname%5D=x), and it stored the URL unredacted when the host was malformed. It now redacts the raw query pairs and keeps every other byte. Sensitive query parameters inLocation,X-Inertia-LocationandRefererheaders are redacted too. Configured keys were also redacted in the entry's own structure: a prop namedtokenlost its metadata, and a key such asidreplaced the entry's id, so the entry could no longer be opened. Keys are now redacted only in application values, and the entry's URLs are still redacted whole when a key such asurlis configured. The DevTools documentation now says which data is redacted, that other bodies, such as HTML or plain text, are stored as sent, and that the gate controls who may view entries, not whose requests are recorded. Upstream has the same bugs._meta.jsoncouldn't be opened or locked, so saved entries never appeared in the listing or reached pruning. That now fails like any other storage failure, and the entry file is only written once the index is locked, so a failed save leaves no unlisted file behind. Entries without a tab ID, such as initial page loads and requests made without the extension, were bounded only by age; the existing per-tab limit now caps them as one group. The failure breaker set its backoff after logging, so a logger failing on the same full disk escaped into the response and left every request retrying. The backoff now comes first, and a failure to log is ignored. Upstream has the same bugs.InertiaState, andInertia::flushShared()clears both. Props from a sharedProvidesInertiaPropertiesprovider are now marked shared, and amatchOn()prop is shown as a deep merge only when it merges. The last three are upstream bugs too. The tag that lets the extension find the initial page's entry is now also added when the root view closes its body as</BODY>, which upstream misses. Adding the tag also kept aContent-Lengththe application set for the page, so the page arrived cut short. The header is now removed once the tag is added; upstream has the same bug.The full test suite, the package metadata and facade docblock checks, formatting and static analysis pass locally. CI runs the full suite and supported service matrix.
Summary by cubic
Adds server-side Inertia DevTools support and routes SSR requests through Hypervel's HTTP client, and adds read-only sessions so polling endpoints can't overwrite data saved by concurrent requests.
New Features
/_inertia/devtools/entries. Recording defaults to the local environment (orINERTIA_DEVTOOLS_ENABLED); outside local, the endpoints require the configured gate, which limits viewing only — every visitor's requests are recorded while it's enabled.readOnlySession(). A read-only session still starts, so auth works, but it is never saved, garbage-collected, recorded as the previous URL, or given a new session cookie. DevTools entry routes use it, so the extension's fetches can't overwrite session data saved by concurrent requests.Inertia::configureSsrRequestUsing()receives thePendingRequestfor each SSR render, health check and shutdown request. SSR requests now run on aninertia-ssrconnection whose shared handler keeps connections open, soHttp::fake()andHttp::preventStrayRequests()apply to them; the testing-onlyHttpGateway::useTestingClient()is removed and the package no longer requires Guzzle directly.Bug Fixes
@inertiadirective and<x-inertia::app>encode page JSON withJSON_HEX_TAG, so a prop containing</script>or<!--can't break the script tag early.JsonSerializableprop are resolved,loadDeferredProps()stops confusing a group named after a global function with a callback, SSR bodies are decoded withjson_decode()to keep client-side fallback working on malformed bodies, the Blade component namespace registers on the resolved compiler, and the Guzzle 7 floor is raised to^7.15.2framework-wide. Packages using them now declarehypervel/collectionsandhypervel/filesystemdirectly.Written for commit ea40c98. Summary will update on new commits.
Note
Add Inertia DevTools request recorder and read-only sessions, and move SSR to the HTTP client
RequestRecorder,Collector,EntriesRepository,EntryStore, sensitive-data redaction, and authorized entry endpoints. Recording defaults to the local environment and is configured through inertia.phpHttpGatewaywith the named Hypervel HTTP connection. AddsConfiguresSsrRequestsandResponseFactory.configureSsrRequestUsingso callers can modify outgoing SSR requests; null SSR timeouts fall back to global HTTP optionsJSON_HEX_TAG) in theAppcomponent and Inertia directivecomposer.jsonmanifests, and stops header, multipart, and structured-data normalization from mutating caller-owned arraysHttpGateway::useTestingClientand the protectedssrClientfactory are removed; use the Hypervel HTTP facade fakes instead.StartSessionandPreventRequestForgerynow skip cookie emission for read-only sessionsMacroscope summarized ea40c98.