Skip to content

Sync Inertia updates and add DevTools support - #52

Merged
binaryfire merged 27 commits into
0.4from
upstream-sync-framework-17
Oct 3, 2026
Merged

binaryfire merged 27 commits into
0.4from
upstream-sync-framework-17

Conversation

@binaryfire

@binaryfire binaryfire commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

This brings Hypervel's Inertia adapter up to date with inertiajs/inertia-laravel 3.x at 4da52b72da, 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.

  • 892, 894, 895, 896 and 897 add the server side of Inertia DevTools. While it's enabled, the adapter records each request to local JSON files: props and their Inertia types, where shared props and the render came from, the route, headers and bodies. The browser extension reads them from /_inertia/devtools/entries. Recording is limited to the local environment unless INERTIA_DEVTOOLS_ENABLED says 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/docs v3/advanced/devtools.mdx at c6a69bd613.
  • 916 adds Inertia::configureSsrRequestUsing(), which receives the PendingRequest for 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 an inertia-ssr connection registered at boot with the configured timeouts. The connection's shared 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, 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.
  • A callback passed to 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 to null uses the HTTP client's global timeout, as Laravel's adapter does by default. A configured throw() or retry() doesn't hide the SSR server's error: its structured response still reaches SsrRenderFailed, rather than being treated as a connection failure that starts the backoff. The Vite documentation covers configuring the request, adapted from inertiajs/docs v3/advanced/server-side-rendering.mdx at cf513d8ffc, and the README's differences now describe the timeouts. docs/todo.md records benchmarking Swoole's coroutine HTTP client for this connection once the HTTP client supports it as a transport.
  • 906 registers the Blade component namespace on the compiler passed to the resolving callback. The Blade facade could resolve a different compiler from the one being built.
  • 910 declares Hypervel\Http\RedirectResponse as Inertia::back()'s return type, which is what Redirect::back() returns, instead of Symfony's base class, so helpers such as with() type-check on the result. Its $fallback parameter is narrowed from mixed to bool|string, matching Redirector::back(), which rejects anything else.
  • 902 resolves closures and Inertia prop types inside a JsonSerializable prop. They were passed through untouched.
  • 908 fixes loadDeferredProps() in tests when a deferred group is named after a global function, such as auth. The group was taken for the callback and the assertion failed with a TypeError.
  • 911 encodes the page JSON in the @inertia directive and the <x-inertia::app> component with JSON_HEX_TAG, so a prop containing </script> or <!-- can't close the script tag early.
  • 891 adds Guzzle 8 support, which Hypervel already had. It also raises the Guzzle 7 floor to ^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.
  • Two SSR gateway tests are ported: a failed render returns null when throw_on_error is disabled (817), and a configured hot URL returns the rendered head and body (885). The gateway already matched upstream.
  • 848's SSR state isolation test now uses upstream's name and dispatches through InertiaState::dispatchSsr(), as upstream's does through SsrState. SsrException also declares its members in upstream's order.

Additional Hypervel Fixes

  • DevTools pruned old entries in its listener, so a storage failure while pruning became a 500. Pruning now runs inside the entry store's flush, behind the same failure breaker as saving. A missing, empty or corrupt index was treated as empty, so the next save dropped every earlier entry from it. The index is now rebuilt from the entry files under its lock, without overwriting an entry saved after the index was read. Upstream has the same bugs.
  • DevTools records nested props under their dotted path, which skipped key-based redaction, so a value such as auth.token was stored unredacted. A value is now redacted when any part 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, while an explicit empty list still turns them off. Upstream has the same bugs.
  • SSR response bodies are decoded with json_decode() rather than Response::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.
  • The HTTP client walked every structured request's payload three times, once over data it had already normalized. On a 6 KB JSON payload that added about 0.24 ms of client CPU per request over raw Guzzle, and about 1.5 ms on a large one. The repeated walk is gone and the rest use a loop that skips scalar values, bringing that down to about 0.16 ms and 0.6 ms. Key order, the transmitted JSON and the handling of Stringable, JsonSerializable and Arrayable values are unchanged.
  • The HTTP client's header, multipart and fake response header normalizers wrote normalized values back into the caller's array, so a value passed by reference changed in place: a Stringable header became a string, and a Stringable multipart 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.
  • The HTTP client's get(), head(), query(), post(), patch(), put() and delete() documented only ConnectionException, so static analysis reported a correct RequestException catch around them as unreachable. They now also document RequestException, which they throw with throw(), throwIf() or a retry() that runs out of attempts. Laravel has the same gap.
  • The api-client, concurrency, grpc, inertia and object-pool packages now require hypervel/collections, which they use directly but only received through other packages. Inertia also requires hypervel/filesystem for DevTools.
  • Routes can now read the session without saving it, with ->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 or XSRF-TOKEN cookie is sent. Route caching keeps the option, and the session documentation covers it.
  • DevTools' entry routes now use read-only sessions. The extension fetches entries while the application's own requests are in flight, so saving the entry request's session could overwrite newer session data, not only the flash data a redirect was about to read. Upstream's PreserveFlashData and PreventPreviousUrlTracking middleware, which covered only the flash data and the previous URL, are removed.
  • DevTools redaction rebuilt URLs with Uri, which rewrote parameters it didn't redact (q=a+b became q=a%2Bb, and filter.name=x became filter%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 in Location, X-Inertia-Location and Referer headers are redacted too. Configured keys were also redacted in the entry's own structure: a prop named token lost its metadata, and a key such as id replaced 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 as url is 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.
  • DevTools recorded a rendered page even when it never reached the client: a page replaced by the version-change 409, or one whose root view failed to render, was recorded against the response that replaced it. The page is now recorded only after its response is built, and dropped when the middleware replaces an Inertia request's page. Upstream has the same bugs.
  • DevTools skipped an index update silently when _meta.json couldn'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.
  • DevTools share sources lived on the per-request recorder, so props shared during boot lost their source on a real server, where each request runs in its own coroutine. They now live beside the shared props in InertiaState, and Inertia::flushShared() clears both. Props from a shared ProvidesInertiaProperties provider are now marked shared, and a matchOn() 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 a Content-Length the 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 test client copied a read-only request's unsaved session changes back to the test, so the next request read and saved them. It now keeps the session the test had before that request. Followed redirects ran inside the first request's coroutine after it had copied its state back, so their session, authentication and request state never reached the test, and a flash message a followed page had already read showed up again on the next request. Redirects are now followed from the test coroutine.

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 records each request (props with their Inertia types, shared-prop and render sources, route, headers, bodies) to local JSON entries and serves them to the browser extension from /_inertia/devtools/entries. Recording defaults to the local environment (or INERTIA_DEVTOOLS_ENABLED); outside local, the endpoints require the configured gate, which limits viewing only — every visitor's requests are recorded while it's enabled.
  • Routes can read the session without saving it via 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 the PendingRequest for each SSR render, health check and shutdown request. SSR requests now run on an inertia-ssr connection whose shared handler keeps connections open, so Http::fake() and Http::preventStrayRequests() apply to them; the testing-only HttpGateway::useTestingClient() is removed and the package no longer requires Guzzle directly.

Bug Fixes

  • The HTTP client's request data normalizer no longer re-walks already-normalized data and builds fresh arrays for headers, multipart and fake response headers, so it never mutates caller values passed by reference. The extra walk cost about 0.24 ms per request over raw Guzzle (1.5 ms on a large payload); it now adds about 0.16 ms and 0.6 ms.
  • DevTools entry storage and redaction fixes beyond upstream: pruning runs under the same failure breaker as saving, index failures throw instead of silently dropping entries, entries recorded without a tab are limited as one group, pages are recorded only after their response builds so a failed root view or version-mismatch 409 isn't stored, configured redaction keys never touch the entry's own id or route but a URL held under one is redacted whole, URL redaction keeps every non-sensitive byte and covers the Location/Referer headers, share sources live in the coroutine state beside the shared props, the recorded entry's id script is injected before a closing body tag of any case, and the injected tag drops a stale Content-Length so the lengthened page is not cut short.
  • The test client now keeps the pre-request session after a read-only request and follows redirects from the test coroutine, so the session state seen by the next request matches the stored state.
  • The @inertia directive and <x-inertia::app> encode page JSON with JSON_HEX_TAG, so a prop containing </script> or <!-- can't break the script tag early.
  • Upstream Inertia syncs: closures and prop types inside a JsonSerializable prop are resolved, loadDeferredProps() stops confusing a group named after a global function with a callback, SSR bodies are decoded with json_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.2 framework-wide. Packages using them now declare hypervel/collections and hypervel/filesystem directly.

Written for commit ea40c98. Summary will update on new commits.

Review in cubic

Note

Add Inertia DevTools request recorder and read-only sessions, and move SSR to the HTTP client

  • Adds an Inertia DevTools subsystem: RequestRecorder, Collector, EntriesRepository, EntryStore, sensitive-data redaction, and authorized entry endpoints. Recording defaults to the local environment and is configured through inertia.php
  • Replaces the raw Guzzle client in HttpGateway with the named Hypervel HTTP connection. Adds ConfiguresSsrRequests and ResponseFactory.configureSsrRequestUsing so callers can modify outgoing SSR requests; null SSR timeouts fall back to global HTTP options
  • Adds read-only sessions: routes can mark sessions read-only, which skips persistence, garbage collection, session cookies, and XSRF-token emission, while in-request changes stay visible
  • Fixes HTTP client and SSR page-data HTML tag escaping (JSON_HEX_TAG) in the App component and Inertia directive
  • Raises the Guzzle 7 constraint to 7.15.2 across all composer.json manifests, and stops header, multipart, and structured-data normalization from mutating caller-owned arrays
  • Behavioral Change: HttpGateway::useTestingClient and the protected ssrClient factory are removed; use the Hypervel HTTP facade fakes instead. StartSession and PreventRequestForgery now skip cookie emission for read-only sessions

Macroscope summarized ea40c98.

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.
@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: hypervel/components-backup/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 60ef17f2-cfbd-4b53-be21-aaa6063c1f81

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Sync Inertia 3.x, add DevTools, and modernize SSR requests

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add coroutine-isolated Inertia DevTools recording, protected entry endpoints, and sensitive-data
 redaction.
• Route configurable SSR requests through Hypervel's HTTP client, including its fakes and retries.
• Fix Inertia rendering and HTTP normalization, strengthen dependencies, and expand regression
 coverage.
Diagram

graph TD
  A["Inertia middleware"] --> B["Request recorder"] --> C["Entry builder"] --> D["Entry store"] --> E[("JSON repository")]
  E --> F["Entry endpoints"] --> G["DevTools extension"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep a dedicated Guzzle SSR client
  • ➕ Avoids the HTTP client's additional per-request CPU cost.
  • ➖ Cannot use standard HTTP fakes and stray-request protection.
  • ➖ Requires a separate configuration and testing path.
2. Store DevTools entries in a database
  • ➕ Could provide centralized querying and retention across workers or hosts.
  • ➖ Adds schema and operational requirements to an optional development tool.
  • ➖ Departs from the upstream extension's file-backed integration.

Recommendation: Keep the PR's HTTP-client SSR integration and file-backed DevTools design. Shared HTTP behavior and upstream compatibility outweigh the measured SSR overhead, while atomic file writes and index recovery address the principal local-storage risks. Document that deployments on multiple hosts need shared storage if the extension must see entries across hosts.

Files changed (90) +7697 / -387

Enhancement (26) +3176 / -104
StopSsr.phpUse the configurable SSR shutdown request +2/-2

Use the configurable SSR shutdown request

• Adapts the stop command to the gateway's HTTP-client-backed shutdown behavior.

src/inertia/src/Commands/StopSsr.php

Collector.phpCollect rendered page metadata +321/-0

Collect rendered page metadata

• Tracks prop types and values, shared keys, route details, component paths, and render and share source locations for an Inertia page.

src/inertia/src/DevTools/Collector.php

IncomingEntry.phpDefine the recorded entry payload +96/-0

Define the recorded entry payload

• Provides the structured DevTools entry and its serialization for persisted request, page, and HTTP metadata.

src/inertia/src/DevTools/Data/IncomingEntry.php

PropType.phpDefine DevTools prop type values +15/-0

Define DevTools prop type values

• Introduces the prop classification values expected by the DevTools entry format.

src/inertia/src/DevTools/Data/PropType.php

RequestType.phpDefine DevTools request types +17/-0

Define DevTools request types

• Introduces request categories used to distinguish navigation, partial, deferred, and other traffic.

src/inertia/src/DevTools/Data/RequestType.php

DevTools.phpGate request recording +97/-0

Gate request recording

• Enables DevTools locally by default, honors explicit configuration, and excludes configured paths before resolving a recorder. Supplies defaults when configuration lists are omitted.

src/inertia/src/DevTools/DevTools.php

DevToolsHeader.phpHandle DevTools correlation headers +69/-0

Handle DevTools correlation headers

• Centralizes reading and writing headers used to associate entries with tabs, visits, parents, and responses.

src/inertia/src/DevTools/DevToolsHeader.php

DevToolsServiceProvider.phpWire DevTools services and routes +82/-0

Wire DevTools services and routes

• Registers request-scoped recording services, the entry repository, and protected entry endpoints. Registers the flush listener only when DevTools is enabled at boot.

src/inertia/src/DevTools/DevToolsServiceProvider.php

EntriesRepository.phpPersist and recover DevTools entries +438/-0

Persist and recover DevTools entries

• Atomically stores JSON entries, maintains an indexed listing, and enforces retention and per-tab limits. Rebuilds missing or corrupt indexes from entry files under a lock.

src/inertia/src/DevTools/EntriesRepository.php

EntryStore.phpFlush entries behind a failure breaker +109/-0

Flush entries behind a failure breaker

• Redacts and persists pending entries after requests, then applies limits and pruning. Suppresses repeated storage failures without turning them into response errors.

src/inertia/src/DevTools/EntryStore.php

Authorize.phpProtect DevTools entry endpoints +41/-0

Protect DevTools entry endpoints

• Allows local access and requires the configured authorization gate outside the local environment.

src/inertia/src/DevTools/Http/Authorize.php

EntriesController.phpExpose recorded entry APIs +70/-0

Expose recorded entry APIs

• Provides filtered entry listings and individual entry retrieval for the browser extension.

src/inertia/src/DevTools/Http/EntriesController.php

IncomingEntryBuilder.phpBuild safe request and response entries +587/-0

Build safe request and response entries

• Combines HTTP details with collected page metadata, classifies requests, and captures bounded, sanitized bodies. Redacts sensitive keys in dotted nested-prop paths.

src/inertia/src/DevTools/IncomingEntryBuilder.php

PropClassifier.phpClassify Inertia prop metadata +133/-0

Classify Inertia prop metadata

• Derives wrapper types, deferred groups, reset and once flags, and merge metadata for recorded props.

src/inertia/src/DevTools/PropClassifier.php

RedactsSensitiveData.phpRedact sensitive entry surfaces +253/-0

Redact sensitive entry surfaces

• Redacts configured keys, headers, and URL query values and sanitizes data before JSON storage. Uses shipped redaction defaults for omitted configuration lists.

src/inertia/src/DevTools/RedactsSensitiveData.php

RequestAttribute.phpName DevTools request attributes +25/-0

Name DevTools request attributes

• Defines request-attribute keys used to pass timing and collected page data through the response lifecycle.

src/inertia/src/DevTools/RequestAttribute.php

RequestRecorder.phpRecord the Inertia request lifecycle +361/-0

Record the Inertia request lifecycle

• Coordinates request timing, shared and resolved props, render sources, response correlation, and entry construction while keeping state scoped to the request.

src/inertia/src/DevTools/RequestRecorder.php

SourceLocator.phpLocate application render and share calls +277/-0

Locate application render and share calls

• Finds application source locations for routes, renders, and shared props while skipping framework-owned paths, including path-repository and monorepo layouts.

src/inertia/src/DevTools/SourceLocator.php

InertiaServiceProvider.phpRegister SSR connection and DevTools +27/-5

Register SSR connection and DevTools

• Registers the named HTTP connection with SSR timeouts and installs the DevTools provider. Registers Blade components on the compiler being resolved and captures route-definition sources when recording.

src/inertia/src/InertiaServiceProvider.php

InertiaState.phpScope SSR request configuration +5/-0

Scope SSR request configuration

• Stores the SSR request callback in Inertia state so boot configuration is inherited while request-time overrides remain isolated.

src/inertia/src/InertiaState.php

Middleware.phpReport middleware lifecycle to DevTools +14/-1

Report middleware lifecycle to DevTools

• Records request starts, shared props, and final responses through the optional request-scoped recorder.

src/inertia/src/Middleware.php

Response.phpAttach page-render events to recording +3/-0

Attach page-render events to recording

• Reports completed page data and resolved props to the DevTools recorder without adding recorder metadata to client-visible page JSON.

src/inertia/src/Response.php

ResponseFactory.phpExpose SSR configuration and render recording +32/-7

Expose SSR configuration and render recording

• Adds 'configureSsrRequestUsing()', records share and render sources, and narrows 'back()' to Hypervel's redirect response and valid fallback types.

src/inertia/src/ResponseFactory.php

ConfiguresSsrRequests.phpDefine the SSR configuration capability +15/-0

Define the SSR configuration capability

• Adds an optional gateway contract for configuring each outgoing SSR HTTP request.

src/inertia/src/Ssr/ConfiguresSsrRequests.php

HttpGateway.phpSend SSR traffic through Hypervel HTTP +82/-89

Send SSR traffic through Hypervel HTTP

• Uses the named HTTP connection for render, health, and shutdown requests and applies request-specific callbacks. Preserves structured SSR errors across thrown responses and retries while retaining connection-failure backoff and malformed-body fallback.

src/inertia/src/Ssr/HttpGateway.php

Header.phpDeclare additional Inertia headers +5/-0

Declare additional Inertia headers

• Adds header names needed to classify and correlate recorded Inertia requests.

src/inertia/src/Support/Header.php

Bug fix (9) +162 / -25
Factory.phpAvoid mutating fake response headers +10/-5

Avoid mutating fake response headers

• Normalizes fake response headers into fresh arrays instead of writing through references supplied by callers.

src/http/src/Client/Factory.php

PendingRequest.phpStreamline and isolate request normalization +69/-15

Streamline and isolate request normalization

• Skips a redundant walk of normalized request data and avoids unnecessary scalar recursion. Builds fresh header and multipart arrays to preserve caller values, and documents request exceptions on HTTP verb methods.

src/http/src/Client/PendingRequest.php

PreserveFlashData.phpPrevent DevTools polling from consuming flash data +31/-0

Prevent DevTools polling from consuming flash data

• Preserves session flash values while the extension fetches entries after a redirect.

src/inertia/src/DevTools/Http/PreserveFlashData.php

PreventPreviousUrlTracking.phpKeep entry polling out of navigation history +27/-0

Keep entry polling out of navigation history

• Prevents DevTools endpoint requests from replacing the application's previous URL.

src/inertia/src/DevTools/Http/PreventPreviousUrlTracking.php

Directive.phpEscape page JSON inside script tags +1/-1

Escape page JSON inside script tags

• Encodes the '@inertia' page payload with 'JSON_HEX_TAG' to prevent prop text from terminating its script element.

src/inertia/src/Directive.php

Inertia.phpExpose the corrected redirect response type +2/-1

Expose the corrected redirect response type

• Aligns the facade's back-navigation signature with Hypervel's redirect response and accepted fallback types.

src/inertia/src/Inertia.php

PropsResolver.phpResolve serializable nested props and record them +19/-0

Resolve serializable nested props and record them

• Descends into 'JsonSerializable' values to resolve nested closures and Inertia prop wrappers. Reports resolved and rescued props to DevTools when enabled.

src/inertia/src/PropsResolver.php

AssertableInertia.phpDisambiguate deferred-prop test arguments +2/-2

Disambiguate deferred-prop test arguments

• Prevents a deferred group such as 'auth' from being mistaken for a callable when loading deferred props.

src/inertia/src/Testing/AssertableInertia.php

App.phpEscape component page JSON +1/-1

Escape component page JSON

• Applies 'JSON_HEX_TAG' to the Blade app component's page payload so embedded tag text cannot close its script element.

src/inertia/src/View/Components/App.php

Refactor (1) +5 / -5
SsrException.phpAlign SSR exception member declarations +5/-5

Align SSR exception member declarations

• Reorders exception members to match the synchronized upstream adapter without changing their purpose.

src/inertia/src/Ssr/SsrException.php

Tests (32) +4222 / -233
AfterEachTestSubscriber.phpReset DevTools state after tests +1/-0

Reset DevTools state after tests

• Clears DevTools' worker-level recording failure state between tests.

src/testing/src/PHPUnit/AfterEachTestSubscriber.php

HttpClientTest.phpTest nonmutating HTTP normalization +83/-0

Test nonmutating HTTP normalization

• Adds reference-based cases for structured data, headers, multipart parts, and fake response headers to ensure normalization leaves caller values intact.

tests/Http/HttpClientTest.php

StopSsrTest.phpTest HTTP-backed SSR shutdown +36/-22

Test HTTP-backed SSR shutdown

• Adapts stop-command coverage to HTTP fakes and the gateway's new shutdown request behavior.

tests/Inertia/Commands/StopSsrTest.php

ComponentTest.phpTest component script escaping and SSR isolation +14/-3

Test component script escaping and SSR isolation

• Checks that page values containing HTML tag syntax remain inside the app component's script. Aligns the SSR state-isolation case with upstream.

tests/Inertia/ComponentTest.php

CoroutineIsolationTest.phpTest per-coroutine SSR callbacks +30/-0

Test per-coroutine SSR callbacks

• Verifies boot-time SSR configuration inheritance and isolation of callbacks set by concurrent requests.

tests/Inertia/CoroutineIsolationTest.php

AuthorizeGateTest.phpTest DevTools gate decisions +110/-0

Test DevTools gate decisions

• Covers nonlocal gate authorization, denial, local bypass, authenticated users, and session availability.

tests/Inertia/DevTools/AuthorizeGateTest.php

AuthorizeMiddlewareTest.phpTest configurable endpoint middleware +63/-0

Test configurable endpoint middleware

• Checks that custom middleware replaces the default group without bypassing endpoint authorization.

tests/Inertia/DevTools/AuthorizeMiddlewareTest.php

AuthorizeTest.phpTest access defaults and URL history +94/-0

Test access defaults and URL history

• Checks local access, nonlocal denial without a gate, and preservation of the application's previous URL during polling.

tests/Inertia/DevTools/AuthorizeTest.php

CollectorIntegrationTest.phpTest page metadata collection end to end +500/-0

Test page metadata collection end to end

• Exercises prop classifications and redaction, rescued deferred props, source locations, route-defined renders, and isolation of recorder metadata from page JSON.

tests/Inertia/DevTools/CollectorIntegrationTest.php

CoroutineIsolationTest.phpTest concurrent DevTools entry flushing +93/-0

Test concurrent DevTools entry flushing

• Ensures overlapping requests retain separate entries and flush each one on its own request-handled event.

tests/Inertia/DevTools/CoroutineIsolationTest.php

DevToolsTest.phpTest DevTools enablement and defaults +82/-0

Test DevTools enablement and defaults

• Covers environment overrides, excluded paths, disabled recording, and the distinction between omitted and explicitly empty exclusion lists.

tests/Inertia/DevTools/DevToolsTest.php

EntriesRepositoryTest.phpTest entry persistence and index recovery +319/-0

Test entry persistence and index recovery

• Covers JSON storage, listing, retention, limits, index reconstruction, and concurrent index updates.

tests/Inertia/DevTools/EntriesRepositoryTest.php

EntryStoreTest.phpTest flushing, pruning, and failure suppression +173/-0

Test flushing, pruning, and failure suppression

• Checks entry persistence, tab limits, pruning, empty flushes, and the breaker after repository failures.

tests/Inertia/DevTools/EntryStoreTest.php

FlashDataTest.phpTest flash data survival during entry polling +91/-0

Test flash data survival during entry polling

• Verifies the extension's entry request does not consume validation errors or other data flashed for the application.

tests/Inertia/DevTools/FlashDataTest.php

HttpEndpointsTest.phpTest DevTools listing and detail APIs +149/-0

Test DevTools listing and detail APIs

• Covers entry retrieval, invalid IDs, subdirectory deployments, and listing filters and pagination.

tests/Inertia/DevTools/HttpEndpointsTest.php

IncomingEntryBuilderMatrixTest.phpExercise entry-builder request variants +375/-0

Exercise entry-builder request variants

• Adds a matrix of request and response combinations to verify entry classification and captured metadata.

tests/Inertia/DevTools/IncomingEntryBuilderMatrixTest.php

IncomingEntryBuilderTest.phpTest safe body serialization +76/-0

Test safe body serialization

• Checks capture of encodable bodies and graceful treatment of unserializable values without discarding valid siblings.

tests/Inertia/DevTools/IncomingEntryBuilderTest.php

InteractsWithDevToolsStorage.phpShare DevTools storage test setup +66/-0

Share DevTools storage test setup

• Provides test helpers for isolated entry storage and cleanup.

tests/Inertia/DevTools/InteractsWithDevToolsStorage.php

MiddlewareDevToolsDisabledTest.phpTest disabled recording behavior +74/-0

Test disabled recording behavior

• Verifies the middleware's response behavior when DevTools is off and recording hooks should not affect requests.

tests/Inertia/DevTools/MiddlewareDevToolsDisabledTest.php

MiddlewareDevToolsTest.phpTest middleware recording across request types +564/-0

Test middleware recording across request types

• Covers correlation headers, HTML injection, request classifications, body and upload handling, redirects, exclusions, and sensitive-header redaction.

tests/Inertia/DevTools/MiddlewareDevToolsTest.php

PropClassifierTest.phpTest the prop classification contract +298/-0

Test the prop classification contract

• Checks wrapper types, deferred groups, reset and once flags, merge direction, and deep-merge metadata.

tests/Inertia/DevTools/PropClassifierTest.php

RecorderResilienceTest.phpTest recorder failure isolation +81/-0

Test recorder failure isolation

• Ensures malformed exclusion configuration and unusable storage do not fail the application response.

tests/Inertia/DevTools/RecorderResilienceTest.php

RedactsSensitiveDataTest.phpTest recursive and URL redaction +222/-0

Test recursive and URL redaction

• Checks case-insensitive nested-key and query-string redaction, relative URLs, and malformed URL fallback.

tests/Inertia/DevTools/RedactsSensitiveDataTest.php

DirectiveTest.phpTest directive script escaping +12/-0

Test directive script escaping

• Confirms the '@inertia' directive safely encodes page values containing HTML tag syntax.

tests/Inertia/DirectiveTest.php

DevToolsRootViewMiddleware.phpAdd a DevTools root-view fixture +16/-0

Add a DevTools root-view fixture

• Supplies middleware for testing recorded initial-page renders and root-view behavior.

tests/Inertia/Fixtures/DevToolsRootViewMiddleware.php

devtools-app.blade.phpAdd a DevTools Blade view fixture +5/-0

Add a DevTools Blade view fixture

• Provides an app view for HTML injection and initial-page integration tests.

tests/Inertia/Fixtures/devtools-app.blade.php

HttpGatewayTest.phpTest the HTTP-client SSR gateway +389/-203

Test the HTTP-client SSR gateway

• Replaces dedicated-client setup with HTTP fakes and adds coverage for callbacks, timeouts, health checks, shutdown, retries, malformed JSON, structured errors, backoff, and stray requests.

tests/Inertia/HttpGatewayTest.php

InertiaServiceProviderTest.phpTest Blade registration on the resolved compiler +28/-0

Test Blade registration on the resolved compiler

• Checks that the Inertia component namespace is installed on the compiler passed to the resolving callback.

tests/Inertia/InertiaServiceProviderTest.php

PackageMetadataTest.phpAssert revised Inertia dependencies +3/-5

Assert revised Inertia dependencies

• Updates dependency expectations for direct collections and filesystem requirements and removal of direct Guzzle usage.

tests/Inertia/PackageMetadataTest.php

PropsResolverTest.phpTest nested serializable props and recorder hooks +116/-0

Test nested serializable props and recorder hooks

• Covers closures and prop wrappers inside 'JsonSerializable' values and verifies recorder calls occur only when DevTools is enabled.

tests/Inertia/PropsResolverTest.php

ResponseFactoryTest.phpTest SSR request callback registration +36/-0

Test SSR request callback registration

• Verifies configuration reaches capable gateways and unsupported gateways produce an explicit error.

tests/Inertia/ResponseFactoryTest.php

AssertableInertiaTest.phpTest deferred groups named after functions +23/-0

Test deferred groups named after functions

• Checks that a group named 'auth' is treated as a group rather than invoked as a callback.

tests/Inertia/Testing/AssertableInertiaTest.php

Documentation (4) +48 / -2
todo.mdRecord future SSR transport benchmark +4/-0

Record future SSR transport benchmark

• Adds a follow-up to benchmark Swoole's coroutine HTTP client for the SSR connection when supported.

docs/todo.md

frontend.mdDocument Inertia DevTools +29/-0

Document Inertia DevTools

• Explains enabling the extension, protecting endpoints, configuring recording, and retaining or redacting entries.

src/docs/frontend.md

vite.mdDocument configurable SSR HTTP requests +14/-1

Document configurable SSR HTTP requests

• Shows how to configure SSR requests through Inertia's new callback and clarifies SSR timeout behavior.

src/docs/vite.md

README.mdClarify SSR timeout differences +1/-1

Clarify SSR timeout differences

• Updates the Hypervel-versus-upstream notes to describe its SSR timeout defaults.

src/inertia/README.md

Other (18) +84 / -18
composer.jsonRaise root Guzzle 7 minimum +1/-1

Raise root Guzzle 7 minimum

• Requires Guzzle 7.15.2 or newer on the 7.x line while retaining Guzzle 8 compatibility.

composer.json

composer.jsonDeclare collections and safer Guzzle versions +2/-1

Declare collections and safer Guzzle versions

• Adds the directly used collections package and raises the Guzzle 7 minimum.

src/api-client/composer.json

composer.jsonRaise broadcasting Guzzle minimum +1/-1

Raise broadcasting Guzzle minimum

• Requires the patched Guzzle 7 release or a compatible Guzzle 8 release.

src/broadcasting/composer.json

composer.jsonDeclare direct collections dependency +1/-0

Declare direct collections dependency

• Explicitly requires the collections package used by concurrency code.

src/concurrency/composer.json

composer.jsonRaise console Guzzle minimum +1/-1

Raise console Guzzle minimum

• Updates the split-package Guzzle constraint to exclude affected 7.x releases.

src/console/composer.json

composer.jsonRaise foundation Guzzle minimum +1/-1

Raise foundation Guzzle minimum

• Aligns the split package with the patched Guzzle 7 floor.

src/foundation/composer.json

composer.jsonDeclare gRPC collections dependency +1/-0

Declare gRPC collections dependency

• Requires the collections package imported directly by gRPC code.

src/grpc/composer.json

composer.jsonRaise HTTP package Guzzle minimum +1/-1

Raise HTTP package Guzzle minimum

• Excludes vulnerable Guzzle 7 releases from the HTTP client's dependency constraint.

src/http/composer.json

composer.jsonDeclare Inertia's direct dependencies +2/-1

Declare Inertia's direct dependencies

• Adds collections and filesystem requirements for direct imports and DevTools storage, and removes the direct Guzzle requirement following the SSR client migration.

src/inertia/composer.json

inertia.phpConfigure DevTools and SSR timeouts +65/-4

Configure DevTools and SSR timeouts

• Adds DevTools enablement, exclusions, storage, middleware, gate, and redaction defaults. Allows nullable, fractional SSR timeouts so global HTTP-client options can apply.

src/inertia/config/inertia.php

composer.jsonRaise notifications Guzzle minimum +1/-1

Raise notifications Guzzle minimum

• Updates the package constraint to the patched Guzzle 7 floor.

src/notifications/composer.json

composer.jsonDeclare object-pool collections dependency +1/-0

Declare object-pool collections dependency

• Requires the collections package used directly by object-pool code.

src/object-pool/composer.json

composer.jsonRaise OpenTelemetry Guzzle minimum +1/-1

Raise OpenTelemetry Guzzle minimum

• Excludes affected Guzzle 7 versions from this split package.

src/opentelemetry/composer.json

composer.jsonRaise Saloon Guzzle minimum +1/-1

Raise Saloon Guzzle minimum

• Aligns the Saloon integration's Guzzle constraint with the patched floor.

src/saloon/composer.json

composer.jsonRaise Scout Guzzle minimum +1/-1

Raise Scout Guzzle minimum

• Updates the split-package constraint to require patched Guzzle 7 versions.

src/scout/composer.json

composer.jsonRaise Sentry Guzzle minimum +1/-1

Raise Sentry Guzzle minimum

• Prevents resolution of affected Guzzle 7 releases.

src/sentry/composer.json

composer.jsonRaise Socialite Guzzle minimum +1/-1

Raise Socialite Guzzle minimum

• Raises the Guzzle 7 dependency floor for the Socialite package.

src/socialite/composer.json

composer.jsonRaise Telescope Guzzle minimum +1/-1

Raise Telescope Guzzle minimum

• Aligns Telescope's split-package constraint with the patched Guzzle 7 floor.

src/telescope/composer.json

@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Inertia adapter updates with DevTools feature and session handling changes.

The PR appears safe to merge based on the accepted findings and the state of the previous threads.

Summary

This PR adds Inertia DevTools recording, routes SSR through Hypervel's HTTP client, introduces read-only sessions, and updates HTTP request normalization. Since the previous review, it masks configured URL fields in recorded entries and removes a stale Content-Length after DevTools tag injection.

Reviews (5) · Last reviewed commit: "Drop a stale Content-Length after adding..."

Comment thread src/inertia/src/DevTools/RedactsSensitiveData.php Outdated
Comment thread src/inertia/src/DevTools/IncomingEntryBuilder.php
Comment thread src/inertia/src/DevTools/RequestRecorder.php
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Redirect headers retain secret query values ✓ Resolved
Description
IncomingEntryBuilder::build() records response headers through redactHeaders(), which does not
redact sensitive query parameters in Location or X-Inertia-Location. When a redirect URL
contains a configured sensitive key such as token, the storage pass cleans redirectLocation but
persists the original value in http.responseHeaders.
Code

src/inertia/src/DevTools/IncomingEntryBuilder.php[50]

+            'responseHeaders' => $this->redactHeaders($response->headers->all()),
Evidence
The builder captures the complete response header bag. Header redaction only matches configured
header names, while URL-query redaction visits values under url and redirectLocation, not
responseHeaders.Location.

src/inertia/src/DevTools/IncomingEntryBuilder.php[43-52]
src/inertia/src/DevTools/IncomingEntryBuilder.php[238-257]
src/inertia/src/DevTools/RedactsSensitiveData.php[79-92]
src/inertia/src/DevTools/RedactsSensitiveData.php[113-139]
src/inertia/src/DevTools/DevTools.php[38-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Redirect response headers retain sensitive query values even after the separate redirect location is redacted.
## Fix Focus Areas
- src/inertia/src/DevTools/IncomingEntryBuilder.php[48-52]
- src/inertia/src/DevTools/RedactsSensitiveData.php[79-92]
- src/inertia/src/DevTools/RedactsSensitiveData.php[185-205]
## Recommended Fix
Apply configured query-key redaction to URL-bearing headers, including Location and X-Inertia-Location, before persisting entries. Test that both the redirect location and response-header copies are redacted.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Raw text bodies retain sensitive values 🐞 Bug ⛨ Security
Description
captureRequestBody() and captureRawResponseBody() store raw UTF-8 text without applying
configured sensitive-key redaction. When an Inertia request has an unparsed text body, or a recorded
response contains text such as token=secret, that value reaches the entry file because the final
storage pass leaves string values unchanged.
Code

src/inertia/src/DevTools/IncomingEntryBuilder.php[291]

+        return $this->captureBodyString($request->getContent() ?: null);
Evidence
Both fallback paths pass text to captureBodyString(), which returns it as the stored value. The
subsequent redaction pass operates on array keys and does not modify string leaves.

src/inertia/src/DevTools/IncomingEntryBuilder.php[263-291]
src/inertia/src/DevTools/IncomingEntryBuilder.php[359-389]
src/inertia/src/DevTools/IncomingEntryBuilder.php[455-465]
src/inertia/src/DevTools/RedactsSensitiveData.php[42-50]
src/inertia/src/DevTools/RedactsSensitiveData.php[60-69]
src/inertia/src/DevTools/EntryStore.php[60-68]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The fallback body-capture paths persist raw text that cannot be protected by key-based array redaction.
## Fix Focus Areas
- src/inertia/src/DevTools/IncomingEntryBuilder.php[277-291]
- src/inertia/src/DevTools/IncomingEntryBuilder.php[359-389]
- src/inertia/src/DevTools/RedactsSensitiveData.php[60-69]
## Recommended Fix
Omit unsupported raw body formats by default, or parse supported formats and redact their sensitive fields before capture. Add tests for text containing a configured sensitive key.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Nested shared props lose their origin 🐞 Bug ≡ Correctness
Description
Collector::addProp() compares each full prop path against a list containing only top-level shared
keys. When a shared value resolves into nested paths, those paths are marked non-shared and can be
dropped from DevTools metadata or considered render-defined instead.
Code

src/inertia/src/DevTools/Collector.php[R103-105]

+        $this->props[$path] = [
+            'shared' => in_array($path, $this->sharedKeys, true),
+            'inertiaType' => $inertiaType?->value,
Evidence
The recorder reduces shared keys to their top-level names, but prop resolution reports dotted nested
paths. The exact-match check marks those paths non-shared; later metadata processing uses that flag
to decide whether to retain or attribute them.

src/inertia/src/DevTools/RequestRecorder.php[102-119]
src/inertia/src/DevTools/RequestRecorder.php[258-276]
src/inertia/src/PropsResolver.php[280-305]
src/inertia/src/DevTools/Collector.php[93-114]
src/inertia/src/DevTools/Collector.php[173-229]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Nested paths produced from a shared prop do not match the collector's top-level shared-key list.
## Fix Focus Areas
- src/inertia/src/DevTools/Collector.php[93-114]
- src/inertia/src/DevTools/RequestRecorder.php[258-276]
- src/inertia/src/DevTools/Collector.php[173-202]
## Recommended Fix
Classify a nested path as shared when its originating top-level prop is shared, while preserving the distinction for render props that override shared values. Test nested shared values and nested props with additional metadata.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/inertia/src/DevTools/IncomingEntryBuilder.php
Comment thread src/inertia/src/DevTools/IncomingEntryBuilder.php
Comment thread src/inertia/src/DevTools/Collector.php
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.
Comment thread src/inertia/src/DevTools/RedactsSensitiveData.php
Comment thread src/routing/src/CompiledRouteCollection.php
Comment thread src/inertia/src/DevTools/EntriesRepository.php
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.
Comment thread src/inertia/src/DevTools/RequestRecorder.php
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.
Comment thread src/inertia/src/DevTools/RedactsSensitiveData.php
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.
@binaryfire
binaryfire merged commit 369f50a into 0.4 Oct 3, 2026
53 checks passed
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.

1 participant